diff --git a/config/routes.ts b/config/routes.ts index 0b014e3..131abee 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -1,4 +1,4 @@ -/** +/** * @name umi 的路由配置 * @description 只支持 path,component,routes,redirect,wrappers,name,icon 的配置 * @param path path 只支持两种占位符配置,第一种是动态参数 :id 的形式,第二种是 * 通配符,通配符只能出现路由字符串的最后。 @@ -80,6 +80,14 @@ export default [ path: '/job-portal/message',// 消息通知页面 component: './JobPortal/Message', }, + { + path: '/job-portal/policy',// 政策列表 + component: './JobPortal/Policy', + }, + { + path: '/job-portal/policy/detail',// 政策详情 + component: './JobPortal/Policy/Detail', + }, ], }, { diff --git a/shihezi.zip b/shihezi.zip index 408afa6..022070b 100644 Binary files a/shihezi.zip and b/shihezi.zip differ diff --git a/src/access.ts b/src/access.ts index f98d2b6..1c45679 100644 --- a/src/access.ts +++ b/src/access.ts @@ -1,4 +1,5 @@ import { checkRole, matchPermission } from './utils/permission'; +import { clearTokenCache } from './utils/tokenCache'; /** * @see https://umijs.org/zh-CN/plugins/plugin-access * */ @@ -58,4 +59,29 @@ export function clearSessionToken() { localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); localStorage.removeItem('expireTime'); + localStorage.removeItem('lcToken'); +} + +/** 退出登录时清空本域全部 localStorage / sessionStorage */ +export function clearAllClientStorage() { + clearSessionToken(); + clearTokenCache(); + const knownKeys = [ + 'userInfo', + 'appUserId', + 'resume_userId', + 'seesionId', + 'internet_token', + 'lang', + 'isDark', + 'iconify-count', + 'iconify-version', + '__DC_STAT_UUID', + ]; + knownKeys.forEach((key) => { + localStorage.removeItem(key); + sessionStorage.removeItem(key); + }); + localStorage.clear(); + sessionStorage.clear(); } diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..c37f57c Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/components/JobPortalHeader/index.less b/src/components/JobPortalHeader/index.less index b8248b9..eb8f8ce 100644 --- a/src/components/JobPortalHeader/index.less +++ b/src/components/JobPortalHeader/index.less @@ -1,35 +1,18 @@ +@import '../../pages/JobPortal/theme.less'; + .job-portal-header { - background: #000000; - color: white; - box-shadow: 0 4px 20px rgba(30, 58, 138, 0.3); position: relative; overflow: hidden; width: 100%; + background: transparent; - // 添加装饰性背景元素 - &::after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: radial-gradient(circle at 20% 80%, rgba(255, 255, 255, 0.1) 0%, transparent 50%), - radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.05) 0%, transparent 50%); - pointer-events: none; - z-index: 1; - } - - // 背景图片轮播容器 .background-carousel { position: absolute; - top: 0; + top: @jp-nav-height; left: 0; right: 0; bottom: 0; z-index: 0; - width: 100%; - height: 100%; .background-slide { position: absolute; @@ -37,8 +20,6 @@ left: 0; right: 0; bottom: 0; - width: 100%; - height: 100%; background-size: cover; background-position: center center; background-repeat: no-repeat; @@ -49,316 +30,352 @@ opacity: 1; } } + + &::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient( + 180deg, + rgba(24, 144, 255, 0.2) 0%, + rgba(0, 0, 0, 0.12) 100% + ); + z-index: 1; + } } - // 顶部导航栏 - .header-nav { - padding: 16px 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.15); + // 顶部栏:问候 + 导航 + 快捷操作 + .top-utility-bar { position: relative; - z-index: 1; - .nav-container { - max-width: 1200px; + z-index: 3; + background: @jp-nav-bg; + border-bottom: 1px solid rgba(255, 255, 255, 0.15); + + .utility-container { + max-width: @jp-content-width; margin: 0 auto; padding: 0 20px; display: flex; - justify-content: space-between; align-items: center; + gap: 16px; + min-height: @jp-nav-height; - .nav-left { - .logo { - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - padding: 8px 16px; - border-radius: 8px; - background: rgba(255, 255, 255, 0.1); - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.2); + .header-brand { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; - &:hover { - transform: translateY(-2px) scale(1.02); - background: rgba(255, 255, 255, 0.15); - box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); - } + .header-logo { + height: 32px; + width: auto; + display: block; + object-fit: contain; + } - .logo-text { - font-size: 26px; - font-weight: 700; - color: white; - text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - letter-spacing: 0.5px; - background: linear-gradient(45deg, #ffffff, #e0f2fe); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - } + .header-title { + font-size: 18px; + font-weight: 700; + color: #fff; + white-space: nowrap; + letter-spacing: 1px; } } - .nav-right { + .top-nav { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + gap: 4px; + min-width: 0; + overflow-x: auto; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + .nav-btn { - color: white; + color: rgba(255, 255, 255, 0.9); border: none; background: transparent; - font-size: 16px; - height: auto; - padding: 10px 20px; - border-radius: 8px; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + font-size: 15px; + height: @jp-nav-height; + padding: 0 20px; + border-radius: 0; position: relative; - overflow: hidden; - - &::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); - transition: left 0.5s; - } - - &:hover { - background: rgba(255, 255, 255, 0.15); - color: white; - transform: translateY(-2px); - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); - - &::before { - left: 100%; - } - } - - &.active { - background: linear-gradient(90deg,#1890ff 40%,#5db5f6 100%); - color: #fff !important; - font-weight: bold; - box-shadow: 0 2px 8px #1572de22; - } + flex-shrink: 0; + transition: color 0.2s; .anticon { margin-right: 6px; - font-size: 16px; + } + + &:hover { + color: #fff; + background: rgba(255, 255, 255, 0.1); + } + + &.active { + color: #fff; + font-weight: 600; + background: transparent; + + &::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: calc(100% - 24px); + height: 3px; + background: #fff; + border-radius: 2px 2px 0 0; + } + } + } + } + + .utility-actions { + flex-shrink: 0; + display: flex; + align-items: center; + + .nav-btn.user-btn, + .nav-btn.login-btn { + position: relative; + color: rgba(255, 255, 255, 0.95); + height: @jp-nav-height; + padding: 0 12px; + + &:hover { + color: #fff; + background: rgba(255, 255, 255, 0.1); } } - .user-info { + .nav-btn.user-btn { + &.active { + color: #fff; + font-weight: 600; + + &::after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: calc(100% - 16px); + height: 3px; + background: #fff; + border-radius: 2px 2px 0 0; + } + } + } + } + } + } + + // Banner:搜索 + .banner-section { + position: relative; + z-index: 2; + min-height: 100px; + display: flex; + align-items: center; + + .banner-inner { + max-width: @jp-content-width; + margin: 0 auto; + padding: 20px; + width: 100%; + box-sizing: border-box; + + .search-section { + width: 100%; + + .search-panel { + width: 100%; + box-sizing: border-box; + background: rgba(255, 255, 255, 0.98); + border-radius: @jp-radius-lg; + padding: 12px 14px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12); + } + + .search-bar { + display: flex; + align-items: center; + width: 100%; + height: 40px; + background: #fff; + border: 1px solid @jp-border; + border-radius: @jp-radius; + padding: 0 4px 0 8px; + overflow: hidden; + box-sizing: border-box; + + .search-type-select { + flex-shrink: 0; + width: 88px !important; + + .ant-select-selector { + border: none !important; + box-shadow: none !important; + background: transparent !important; + padding: 0 8px !important; + height: 38px !important; + line-height: 38px !important; + } + + .ant-select-selection-item { + font-size: 14px; + color: @jp-text-primary; + font-weight: 500; + } + } + + .search-divider { + flex-shrink: 0; + width: 1px; + height: 22px; + background: @jp-border; + margin: 0 4px; + } + + .search-input { + flex: 1; + min-width: 0; + border: none !important; + box-shadow: none !important; + font-size: 14px; + padding: 0 8px; + + &:focus { + box-shadow: none !important; + } + } + + .search-submit { + flex-shrink: 0; + height: 32px; + margin: 0 2px; + padding: 0 18px; + border-radius: 6px; + font-weight: 500; + background: @jp-primary; + border-color: @jp-primary; + + &:hover { + background: @jp-primary-dark; + border-color: @jp-primary-dark; + } + } + } + + .hot-jobs { display: flex; align-items: center; gap: 10px; - cursor: pointer; - padding: 10px 16px; - border-radius: 12px; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - background: rgba(255, 255, 255, 0.1); - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.2); + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid @jp-border; + min-height: 30px; - &:hover { - background: rgba(255, 255, 255, 0.2); - transform: translateY(-2px); - box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); - } - - .ant-avatar { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - } - - .user-name { - color: white; - font-size: 15px; - font-weight: 600; - text-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); - } - } - } - } - } - - // 搜索区域 - .search-section { - padding: 24px 0 32px; - position: relative; - z-index: 1; - - .search-container { - max-width: 1200px; - margin: 0 auto; - padding: 0 20px; - - .search-bar { - display: flex; - align-items: center; - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(20px); - border-radius: 16px; - padding: 12px 20px; - margin-bottom: 20px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(255, 255, 255, 0.2); - border: 1px solid rgba(255, 255, 255, 0.3); - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - - &:hover { - transform: translateY(-2px); - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(255, 255, 255, 0.3); - } - - .ant-select { - .ant-select-selector { - border: none !important; - box-shadow: none !important; - } - } - - .ant-input { - border: none !important; - box-shadow: none !important; - font-size: 16px; - - &:focus { - box-shadow: none !important; - } - } - - .ant-btn { - height: auto; - padding: 10px 20px; - border-radius: 10px; - font-weight: 600; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - - &.ant-btn-primary { - background: linear-gradient(135deg, #3b82f6, #1d4ed8); - border: none; - box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4); - - &:hover { - background: linear-gradient(135deg, #2563eb, #1e40af); - transform: translateY(-1px); - box-shadow: 0 6px 20px rgba(59, 130, 246, 0.5); - } - } - - &:not(.ant-btn-primary) { - color: #6b7280; - background: transparent; - - &:hover { - color: #3b82f6; - background: rgba(59, 130, 246, 0.1); - } - } - } - } - - .hot-jobs { - .ant-typography { - color: white; - margin-right: 16px; - font-size: 16px; - font-weight: 600; - text-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); - } - - .hot-jobs-container { - display: flex; - flex-wrap: nowrap; - overflow-x: auto; - gap: 12px; - margin-top: 12px; - padding-bottom: 8px; - - &::-webkit-scrollbar { - height: 6px; - } - - &::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.1); - border-radius: 3px; - } - - &::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 3px; - - &:hover { - background: rgba(255, 255, 255, 0.5); - } - } - - .hot-job-tag { - background: rgba(255, 255, 255, 0.15); - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.3); - color: white; - border-radius: 20px; - padding: 8px 16px; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + .hot-jobs-label { flex-shrink: 0; - white-space: nowrap; - max-width: 140px; - overflow: hidden; - text-overflow: ellipsis; + font-size: 13px; font-weight: 500; - font-size: 14px; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + color: @jp-text-secondary; + white-space: nowrap; - &:hover { - background: rgba(255, 255, 255, 0.25); - transform: translateY(-2px) scale(1.05); - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); - border-color: rgba(255, 255, 255, 0.5); + &::after { + content: ':'; } } - } - } - } - } -} -// 响应式设计 -@media (max-width: 768px) { - .job-portal-header { - .header-nav { - .nav-container { - padding: 0 16px; + .hot-jobs-container { + flex: 1; + min-width: 0; + display: flex; + flex-wrap: nowrap; + align-items: center; + gap: 8px; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: none; + -webkit-overflow-scrolling: touch; - .nav-left { - .logo { - .logo-text { - font-size: 20px; - } - } - } - - .nav-right { - .nav-btn { - font-size: 14px; - padding: 6px 12px; - } - - .user-info { - .user-name { + &::-webkit-scrollbar { display: none; } - } - } - } - } - .search-section { - .search-container { - padding: 0 16px; + .hot-job-tag { + flex-shrink: 0; + max-width: 108px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin: 0; + padding: 0 10px; + height: 26px; + line-height: 24px; + font-size: 12px; + border-radius: 13px; + cursor: pointer; + .jp-job-tag(); + transition: all 0.2s; - .search-bar { - .ant-btn { - padding: 6px 12px; - font-size: 14px; + &:hover { + background: @jp-primary !important; + border-color: @jp-primary !important; + color: #fff !important; + } + } } } } } } } + +@media (max-width: 768px) { + .job-portal-header { + .top-utility-bar .utility-container { + flex-wrap: wrap; + padding: 8px 12px; + gap: 8px; + + .header-brand { + .header-logo { + height: 28px; + } + + .header-title { + font-size: 16px; + } + } + + .top-nav { + order: 3; + width: 100%; + justify-content: flex-start; + + .nav-btn { + padding: 0 12px; + font-size: 14px; + } + } + + .utility-actions { + margin-left: auto; + } + } + + .banner-section .banner-inner { + padding: 16px 12px; + } + } +} diff --git a/src/components/JobPortalHeader/index.tsx b/src/components/JobPortalHeader/index.tsx index a20c048..698cfb2 100644 --- a/src/components/JobPortalHeader/index.tsx +++ b/src/components/JobPortalHeader/index.tsx @@ -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 = ({ }; 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 = ({ 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 = ({ 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: , - label: '个人中心', - }, - { - type: 'divider', - }, - { - key: 'logout', - icon: , - label: '退出登录', - danger: true, - }, - ]; - return (
{/* 背景图片轮播 */} @@ -237,108 +238,127 @@ const JobPortalHeader: React.FC = ({ ))}
- {/* 顶部导航栏 */} -
-
-
- {/*
handleNavClick('/job-portal')}> */} -
handleNavClick('http://218.31.252.15:9081/hrss-web-vue/home', true)}> - 石河子智慧就业 -
+ {/* 顶部栏:问候 + 导航 + 快捷操作 */} +
+
+
{ window.location.href = PORTAL_HOME_URL; }}> + 石城智慧就业 + 石城智慧就业
-
- + +
+ {loggedIn ? ( - 0 ? unreadCount : 0} offset={[10, 0]} size="small"> - - - } + onClick={handleGoToLogin} + className="nav-btn login-btn" > - - - + 登录 + + )}
- {/* 搜索区域 */} {showSearch && ( -
-
-
- - - -
- - {/* 热门职位 */} - {showHotJobs && ( -
- 热门职位: -
- {(jobRecommendData?.data?.slice(0, 8) || hotJobs.slice(0, 7)).map((job: any, index: number) => ( - handleHotJobClick(job.jobTitle || job)} - > - {job.jobTitle || job} - - ))} +
+
+
+
+
+ + + +
+ + {showHotJobs && ( +
+ 热门职位 +
+ {getHotJobItems().map((item) => ( + handleHotJobClick(item.title)} + > + {formatHotJobLabel(item.title)} + + ))} +
+
+ )}
- )} -
+
+
)}
); diff --git a/src/pages/JobPortal/Detail/index.less b/src/pages/JobPortal/Detail/index.less index 9207f01..131bb56 100644 --- a/src/pages/JobPortal/Detail/index.less +++ b/src/pages/JobPortal/Detail/index.less @@ -1,222 +1,74 @@ +@import '../theme.less'; + .job-detail-page { min-height: 100vh; - background-color: #f0f2f5; - // padding: 20px; - .job-detail-container { - margin-top: 20px!important; - max-width: 1200px; - margin: 0 auto; + background-color: @jp-bg-page; - .back-button { + .job-detail-container { + .jp-page-container(); + margin-top: 20px !important; + padding-bottom: 40px; + + .back-button .ant-btn { + color: @jp-primary; + padding: 0; + + &:hover { + color: @jp-primary-dark; + } + } + + .job-header-card, + .info-card, + .competitiveness-card, + .skills-card { + .jp-card-base(); margin-bottom: 16px; - .ant-btn { - color: #1890ff; - padding: 0; - - &:hover { - color: #40a9ff; - } + .ant-card-head-title { + .jp-section-title(); + font-size: 18px; } } .job-header-card { - margin-bottom: 16px; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - - .job-title-section { - .job-title { - color: #262626; - font-weight: 600; - margin-bottom: 0; - } - - .job-salary-display { - color: #ff4d4f; - font-size: 24px; - font-weight: 600; - margin-left: 16px; - } - - .job-meta { - color: #8c8c8c; - font-size: 14px; - - .anticon { - margin-right: 4px; - } - } + .job-salary-display { + .jp-salary-text(); + font-size: 24px; } - .job-actions { - display: flex; - flex-direction: column; - gap: 12px; - - .action-buttons { - width: 100%; - display: flex; - - .ant-btn { - flex: 1; - } - } - } - } - - .info-card { - margin-bottom: 16px; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - - .ant-card-head-title { - font-weight: 600; - font-size: 18px; + .job-meta { + color: @jp-text-muted; } - .company-intro { - .company-meta { - color: #8c8c8c; - font-size: 14px; - } - - .ant-typography { - margin-top: 16px; - line-height: 1.8; - color: #595959; - } + .job-actions .ant-btn-primary { + background: @jp-primary; + border-color: @jp-primary; } } .competitiveness-card { - margin-bottom: 16px; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - - .ant-card-head-title { - font-weight: 600; - font-size: 18px; + .score-value { + color: @jp-primary; + font-size: 32px; + font-weight: bold; } - .radar-chart-container { - position: relative; - width: 100%; - - .radar-summary { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - text-align: center; - pointer-events: none; - - .summary-score { - .score-value { - font-size: 32px; - font-weight: bold; - color: #1890ff; - line-height: 1; - } - - .score-label { - font-size: 14px; - color: #8c8c8c; - margin-top: 8px; - } - } - } + .score-label { + color: @jp-text-muted; } } - .skills-card { - margin-bottom: 16px; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - - .ant-card-head-title { - font-weight: 600; - font-size: 18px; - } - - .skill-item { - padding: 12px; - background: #fafafa; - border-radius: 4px; - - .skill-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; - - .anticon { - color: #faad14; - } - } - - .salary-indicator { - display: block; - margin-top: 4px; - font-size: 12px; - color: #8c8c8c; - } - } - } - - // 响应式设计 - @media (max-width: 992px) { - .job-header-card { - .job-title-section { - margin-bottom: 16px; - } - - .job-actions { - width: 100%; - - .action-buttons { - flex-direction: column; - } - } - } - } - - @media (max-width: 768px) { - padding: 10px; - - .job-header-card { - .job-title-section { - .job-title { - font-size: 20px; - } - - .job-salary-display { - font-size: 18px; - margin-left: 12px; - } - } - - .job-actions { - margin-top: 16px; - } - } - - .competitiveness-card { - .radar-chart-container { - .radar-summary { - .summary-score { - .score-value { - font-size: 24px; - } - - .score-label { - font-size: 12px; - } - } - } - } - } + .skills-card .skill-item { + background: @jp-primary-light; + border-radius: @jp-radius; + border: 1px solid @jp-primary-border; } } } +@media (max-width: 768px) { + .job-detail-page .job-detail-container { + padding: 0 12px; + } +} diff --git a/src/pages/JobPortal/Detail/index.tsx b/src/pages/JobPortal/Detail/index.tsx index a8fdf9f..4c4fe90 100644 --- a/src/pages/JobPortal/Detail/index.tsx +++ b/src/pages/JobPortal/Detail/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { useParams, history, useLocation } from '@umijs/max'; import { Card, @@ -27,10 +27,17 @@ import { import { Radar } from '@ant-design/charts'; import JobPortalHeader from '@/components/JobPortalHeader'; import './index.less'; -import { getJobCompetitiveness } from '@/services/jobportal/competitiveness'; +import { + getJobCompetitiveness, + getEmptyCompetitivenessRadar, +} from '@/services/jobportal/competitiveness'; +import { isJobPortalLoggedIn } from '@/utils/jobPortalAuth'; import { favoriteJob, unfavoriteJob, applyJob } from '@/services/jobportal/user'; -import { getAccessToken } from '@/access'; -import { PageEnum } from '@/enums/pagesEnums'; +import { ensureJobPortalLogin, PORTAL_LOGIN_URL } from '@/utils/jobPortalAuth'; +import { getDictLabel, findTreeLabelById } from '@/utils/jobPortalDict'; +import { getDictValueEnum } from '@/services/system/dict'; +import { getCmsIndustryTreeList } from '@/services/classify/industry'; +import type { DictValueEnumObj } from '@/components/DictTag'; const { Title, Text, Paragraph } = Typography; @@ -106,11 +113,13 @@ const transformJobData = (jobData: any) => {

3. 具备良好的团队协作能力;

`, companyInfo: { - name: jobData.companyName || jobData.company, - type: jobData.industry || '互联网/电子商务', - scale: jobData.scale || '100-499人', - industry: jobData.industry || '计算机/互联网', - description: jobData.companyVo?.companyDescription || jobData.companyDescription || '暂无公司描述信息' + name: jobData.companyName || jobData.company || jobData.companyVo?.name, + scale: jobData.companyVo?.scale ?? jobData.scale ?? '', + industry: jobData.companyVo?.industry ?? jobData.industry ?? '', + description: + jobData.companyVo?.companyDescription + || jobData.companyDescription + || '暂无公司描述信息', }, competitiveness: { overall: 75, @@ -157,7 +166,6 @@ const mockJobDetail = { `, companyInfo: { name: '青岛科技发展有限公司', - type: '互联网/电子商务', scale: '500-999人', industry: '计算机/互联网', description: '青岛科技发展有限公司是一家专注于软件开发、技术服务的科技企业。公司致力于为客户提供优质的IT解决方案,业务涵盖企业信息化、移动应用开发等领域。我们拥有一支年轻、专业的团队,注重技术创新和人才培养,为员工提供良好的发展平台。' @@ -193,6 +201,36 @@ const JobDetailPage: React.FC = () => { { item: '年龄', score: 0 }, { item: '工作地', score: 0 } ]); + const [scaleEnum, setScaleEnum] = useState({}); + const [industryTree, setIndustryTree] = useState([]); + + useEffect(() => { + Promise.all([ + getDictValueEnum('scale', true, true), + getCmsIndustryTreeList(), + ]).then(([scaleData, industryRes]) => { + setScaleEnum(scaleData); + if (industryRes?.code === 200 && industryRes?.data) { + setIndustryTree(industryRes.data); + } + }); + }, []); + + const companyMetaItems = useMemo(() => { + const company = originalJobData?.companyVo || {}; + const rawScale = company.scale ?? originalJobData?.scale ?? jobDetail.companyInfo?.scale; + const rawIndustry = + company.industry ?? originalJobData?.industry ?? jobDetail.companyInfo?.industry; + + const items: string[] = []; + const scaleLabel = getDictLabel(scaleEnum, rawScale) || rawScale; + const industryLabel = findTreeLabelById(industryTree, rawIndustry) || rawIndustry; + + if (scaleLabel) items.push(scaleLabel); + if (industryLabel) items.push(industryLabel); + + return items; + }, [originalJobData, jobDetail.companyInfo, scaleEnum, industryTree]); useEffect(() => { // 首先尝试从传递的数据中获取职位信息 @@ -226,10 +264,19 @@ const JobDetailPage: React.FC = () => { }, [jobDetail?.id]); const fetchCompetitiveness = async (jobIdNum: number) => { + if (!isJobPortalLoggedIn()) { + setOverallScore(0); + setRadarData(getEmptyCompetitivenessRadar()); + return; + } try { const res = await getJobCompetitiveness(jobIdNum); const data = res as any; - if (!data) return; + if (!data) { + setOverallScore(0); + setRadarData(getEmptyCompetitivenessRadar()); + return; + } if (typeof data.matchScore === 'number') { const clamped = Math.max(0, Math.min(100, data.matchScore)); setOverallScore(clamped); @@ -248,36 +295,25 @@ const JobDetailPage: React.FC = () => { ].map(d => ({ ...d, score: Math.max(0, Math.min(100, d.score)) })); setRadarData(mapped); } else { - setRadarData([ - { item: '技能', score: 0 }, - { item: '工作经验', score: 0 }, - { item: '学历', score: 0 }, - { item: '薪资', score: 0 }, - { item: '年龄', score: 0 }, - { item: '工作地', score: 0 } - ]); + setRadarData(getEmptyCompetitivenessRadar()); } } catch (e) { - // 保持默认mock数据 + setOverallScore(0); + setRadarData(getEmptyCompetitivenessRadar()); } }; const handleCollect = async () => { - // 检查用户是否登录 - const token = getAccessToken(); - if (!token) { - message.warning('请先登录'); - history.push(PageEnum.LOGIN); + if (!ensureJobPortalLogin('收藏职位')) { return; } // 如果是取消收藏,调用取消收藏接口 if (isFavorited) { - // 获取用户ID const userId = getUserIdFromCache(); if (!userId) { message.warning('无法获取用户信息,请重新登录'); - history.push(PageEnum.LOGIN); + window.location.href = PORTAL_LOGIN_URL; return; } @@ -307,12 +343,10 @@ const JobDetailPage: React.FC = () => { return; } - // 如果是收藏,调用收藏接口 - // 获取用户ID const userId = getUserIdFromCache(); if (!userId) { message.warning('无法获取用户信息,请重新登录'); - history.push(PageEnum.LOGIN); + window.location.href = PORTAL_LOGIN_URL; return; } @@ -342,19 +376,14 @@ const JobDetailPage: React.FC = () => { }; const handleApply = async () => { - // 检查用户是否登录 - const token = getAccessToken(); - if (!token) { - message.warning('请先登录'); - history.push(PageEnum.LOGIN); + if (!ensureJobPortalLogin('申请职位')) { return; } - // 获取用户ID const userId = getUserIdFromCache(); if (!userId) { message.warning('无法获取用户信息,请重新登录'); - history.push(PageEnum.LOGIN); + window.location.href = PORTAL_LOGIN_URL; return; } @@ -471,12 +500,10 @@ const JobDetailPage: React.FC = () => { {jobDetail.companyInfo.name} - - {jobDetail.companyInfo.type} - - {jobDetail.companyInfo.scale} - - {jobDetail.companyInfo.industry} + }> + {companyMetaItems.map((label) => ( + {label} + ))}
diff --git a/src/pages/JobPortal/List/index.less b/src/pages/JobPortal/List/index.less index 6e347df..ac41e9b 100644 --- a/src/pages/JobPortal/List/index.less +++ b/src/pages/JobPortal/List/index.less @@ -1,210 +1,54 @@ +@import '../theme.less'; + .job-list-page { - height: 100vh; - background-color: #f5f5f5; + min-height: 100vh; + background-color: @jp-bg-page; display: flex; flex-direction: column; - // 顶部搜索栏 - .top-header { - background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); - padding: 16px 24px; - border-bottom: none; - display: flex; - justify-content: center; - - .ant-typography { - margin: 0; - color: #fff; - } - - .ant-btn-link { - padding: 0; - height: auto; - color: #fff; - - &:hover { - color: #40a9ff; - } - } - - .ant-input { - &:focus, &:hover { - border-color: #1890ff; - } - } - - .ant-btn:not(.ant-btn-primary):not(.ant-btn-link) { - background: rgba(255, 255, 255, 0.2); - border-color: rgba(255, 255, 255, 0.3); - color: #fff; - - &:hover { - background: rgba(255, 255, 255, 0.3); - border-color: rgba(255, 255, 255, 0.5); - color: #fff; - } - } - - .ant-row { - max-width: 1400px; - width: 100%; - margin: 0; - } - } - - // 筛选栏 - .filter-bar { - background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); - padding: 12px 24px; - border-bottom: none; - display: flex; - justify-content: center; - - .ant-col { - .ant-select { - width: 100%; - background: #fff; - height: 36px; - - .ant-select-selector { - background: #fff !important; - border-radius: 4px; - height: 36px !important; - line-height: 36px !important; - border: 1px solid transparent; - transition: all 0.3s; - - .ant-select-selection-item { - line-height: 34px; - } - - .ant-select-selection-placeholder { - line-height: 34px; - } - } - - &:hover, - &.ant-select-focused { - .ant-select-selector { - border-color: #91d5ff; - box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); - } - } - } - } - - .ant-btn-link { - color: #fff; - font-size: 14px; - padding: 8px 12px; - border-radius: 4px; - transition: all 0.3s; - - &:hover { - background: rgba(255, 255, 255, 0.1); - color: #fff; - } - } - - .ant-row { - max-width: 1400px; - width: 100%; - margin: 0; - display: flex; - justify-content: center; - } - - // 下拉菜单样式 - .ant-select-dropdown { - border-radius: 4px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - - .ant-select-item { - padding: 8px 16px; - transition: all 0.3s; - - &:hover { - background-color: #e6f7ff; - } - - &.ant-select-item-option-selected { - background-color: #bae7ff; - color: #1890ff; - } - } - } - } - - // 主要内容区 .main-content { - max-width: 1400px; + max-width: @jp-content-width; margin: 0 auto; - padding: 16px; + padding: 20px; flex: 1; display: flex; flex-direction: column; overflow: hidden; + width: 100% !important; - .back-button { - margin-bottom: 16px; - - .ant-btn { - color: #1890ff; - padding: 0; - - &:hover { - color: #40a9ff; - } - } - } - - // 职位列表 .job-list { - background: #fff; - border-radius: 8px; + .jp-card-base(); overflow-y: auto; - height: calc(100% - 40px); // 减少20px高度,增加底部距离 - max-height: calc(100vh - 220px); // 相应调整最大高度 - min-height: 380px; // 相应调整最小高度 + height: calc(100% - 40px); + max-height: calc(100vh - 320px); + min-height: 380px; - // 自定义滚动条样式 - 更细更美观 &::-webkit-scrollbar { width: 4px; } - &::-webkit-scrollbar-track { - background: #f8f9fa; - border-radius: 2px; - } - &::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 2px; - transition: all 0.2s ease; - - &:hover { - background: #9ca3af; - } } - // Firefox 滚动条样式 scrollbar-width: thin; - scrollbar-color: #d1d5db #f8f9fa; .job-card { border: none; - border-bottom: 1px solid #f0f0f0; + border-bottom: 1px solid @jp-border; border-radius: 0; cursor: pointer; - transition: all 0.3s; + transition: all 0.2s; + box-shadow: none; &:hover { - background-color: #fafafa; + background-color: @jp-primary-light; } &.active { - background-color: #e6f7ff; - border-left: 3px solid #1890ff; + background-color: @jp-primary-light; + border-left: 3px solid @jp-primary; } .job-card-header { @@ -217,354 +61,217 @@ .job-title { margin: 0; font-size: 16px; - color: #262626; + color: @jp-text-primary; + font-weight: 600; flex: 1; - min-width: 0; // 允许文本截断 - word-break: break-word; // 允许长单词换行 - line-height: 1.4; } .job-salary { - color: #ff4d4f; - font-size: 16px; - font-weight: 600; - flex-shrink: 0; // 防止薪资被压缩 - white-space: nowrap; // 确保薪资在一行显示 - } - } - - .job-card-info { - margin-bottom: 8px; - - .job-company { - color: #595959; - font-size: 14px; - word-break: break-word; - line-height: 1.4; - margin-bottom: 4px; - } - - .job-experience, - .job-education { - color: #8c8c8c; - font-size: 14px; - margin-right: 16px; - } - } - - .job-card-footer { - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 8px; - - .job-company { - color: #595959; - font-size: 14px; - } - - .job-location { - color: #8c8c8c; - font-size: 13px; - - .anticon { - margin-right: 4px; - } - } - - .job-experience, - .job-education { - color: #8c8c8c; - font-size: 12px; + .jp-salary-text(); + font-size: 15px; + flex-shrink: 0; white-space: nowrap; } } - .job-tags { + .job-card-info .job-company { + color: @jp-text-secondary; + font-size: 14px; + } + + .job-card-footer { display: flex; flex-wrap: wrap; - gap: 6px; - margin-top: 8px; + gap: 8px; - .job-tag { - background: #f6f8fa; - border: 1px solid #e1e4e8; - color: #666; - border-radius: 4px; - padding: 2px 8px; + .job-location, + .job-experience, + .job-education { + color: @jp-text-muted; font-size: 12px; - margin: 0; } } + + .job-tags .job-tag { + .jp-job-tag(); + font-size: 12px; + padding: 2px 8px; + } } } - // 右侧详情容器 .job-detail-container { - height: calc(100% - 20px); // 减少20px高度,增加底部距离 + height: calc(100% - 20px); overflow-y: auto; - padding-right: 8px; - - // 隐藏滚动条但保持滚动功能 - scrollbar-width: none; // Firefox - -ms-overflow-style: none; // IE and Edge + scrollbar-width: none; &::-webkit-scrollbar { - display: none; // Chrome, Safari, Opera + display: none; } } - // 职位详情 - .job-detail-card { - background: #fff; - border-radius: 8px; + .job-detail-card, + .company-info-card { + .jp-card-base(); min-height: 600px; + } + .job-detail-card { .job-detail-header { display: flex; justify-content: space-between; align-items: flex-start; - margin-bottom: 16px; + gap: 24px; + padding-bottom: 4px; - .header-left { + .job-header-info { flex: 1; + min-width: 0; - .ant-typography { - margin-bottom: 0; + .job-detail-title { + margin: 0 0 8px; + font-size: 22px; + font-weight: 600; + color: @jp-text-primary; + line-height: 1.35; } .salary-text { - font-size: 24px; - color: #ff4d4f; - font-weight: 600; - margin-left: 0; + display: block; + .jp-salary-text(); + font-size: 20px; + line-height: 1.4; + margin-bottom: 14px; } - .ant-space { + .job-header-tags { display: flex; - align-items: baseline; flex-wrap: wrap; - } + gap: 8px; - > div { - display: flex; - align-items: center; - } + .meta-tag { + .jp-job-tag(); + margin: 0; + padding: 2px 10px; + font-size: 13px; + line-height: 22px; - .ant-btn-circle { - flex-shrink: 0; + .anticon { + margin-right: 4px; + } + } } } - .header-right { - .ant-space { - .ant-btn { - border-radius: 4px; + .job-header-actions { + flex-shrink: 0; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 12px; + + .btn-favorite { + border-radius: @jp-radius; + height: 36px; + padding: 0 16px; + color: @jp-primary; + border-color: @jp-primary; + + &.favorited, + &:hover { + color: #ff4d4f; + border-color: #ff4d4f; + } + } + + .btn-apply { + height: 40px; + padding: 0 28px; + font-size: 15px; + font-weight: 500; + border-radius: @jp-radius; + background: @jp-primary; + border-color: @jp-primary; + } + + .action-icons { + .anticon { + font-size: 18px; + color: @jp-text-muted; + cursor: pointer; + + &:hover { + color: @jp-primary; + } } } } } - .job-basic-info { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; - - .action-icons { - .anticon { - font-size: 16px; - color: #8c8c8c; - cursor: pointer; - - &:hover { - color: #1890ff; - } - } - } + .job-detail-divider { + margin: 20px 0 24px; } .job-description { - margin-bottom: 24px; - - .skill-tags { - margin-bottom: 16px; + .section-title { + margin: 0 0 16px; + font-size: 16px; + font-weight: 600; + color: @jp-text-primary; } .description-text { - line-height: 2; - color: #595959; + margin: 0; + line-height: 1.9; + color: @jp-text-secondary; + white-space: pre-line; + font-size: 14px; } } .competitiveness-card { - margin-bottom: 16px; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + .jp-card-base(); - .ant-card-head-title { - font-weight: 600; - font-size: 18px; - } - - .radar-chart-container { - position: relative; - width: 100%; - - .radar-summary { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - text-align: center; - pointer-events: none; - - .summary-score { - .score-value { - font-size: 32px; - font-weight: bold; - color: #1890ff; - line-height: 1; - } - - .score-label { - font-size: 14px; - color: #8c8c8c; - margin-top: 8px; - } - } - } - } - } - - .benefits { - .benefits-list { - padding-left: 20px; - line-height: 2; - - li { - color: #595959; - margin-bottom: 8px; - } - } - } - - .company-info { - margin-top: 24px; - - .company-detail { - .ant-typography { - margin-bottom: 12px; - } - - .company-description { - color: #595959; - line-height: 1.8; - font-size: 14px; - margin: 0; - } + .score-value { + color: @jp-primary; } } } - // 公司信息卡片 - .company-info-card { - background: #fff; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - - .company-overview { - display: flex; - gap: 16px; - margin-bottom: 20px; - - .ant-avatar { - flex-shrink: 0; - } - - .company-info-right { - flex: 1; - - .company-name { - margin: 0 0 8px 0; - font-size: 18px; - font-weight: 600; - color: #262626; - } - - .company-meta { - display: flex; - align-items: center; - gap: 8px; - - .ant-typography { - font-size: 14px; - color: #8c8c8c; - } - - .ant-divider { - margin: 0; - height: 14px; - } - } - } - } - - .company-description-section { - .company-description { - color: #595959; - line-height: 1.8; - font-size: 14px; - margin: 0; - } - } - } - } -} - -// 响应式设计 -@media (max-width: 1200px) { - .job-list-page { - .main-content { - .job-detail-header { + @media (max-width: 992px) { + .job-detail-card .job-detail-header { flex-direction: column; - .header-left, - .header-right { + .job-header-actions { width: 100%; - margin-bottom: 12px; + flex-direction: row; + justify-content: space-between; + align-items: center; } } } + + .company-info-card { + margin-top: 16px; + + .company-name { + color: @jp-text-primary; + } + + .company-meta .ant-typography { + color: @jp-text-muted; + } + } } } @media (max-width: 768px) { - .job-list-page { - .top-header { - padding: 12px 16px; + .job-list-page .main-content { + padding: 12px; - .ant-col { - margin-bottom: 8px; - } - } - - .filter-bar { - padding: 8px 16px; - overflow-x: auto; - - .ant-col { - min-width: 80px; - margin-bottom: 8px; - } - } - - .main-content { - .job-list { - margin-bottom: 16px; - } + .job-list { + margin-bottom: 16px; + max-height: none; } } } - diff --git a/src/pages/JobPortal/List/index.tsx b/src/pages/JobPortal/List/index.tsx index 66a3878..48c9cbe 100644 --- a/src/pages/JobPortal/List/index.tsx +++ b/src/pages/JobPortal/List/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { Card, Row, @@ -26,12 +26,18 @@ import { import { history, useLocation } from '@umijs/max'; import { getJobList } from '@/services/common/jobTitle'; import { getDictValueEnum } from '@/services/system/dict'; -import DictTag from '@/components/DictTag'; import JobPortalHeader from '@/components/JobPortalHeader'; import { favoriteJob, unfavoriteJob, applyJob } from '@/services/jobportal/user'; -import { getAccessToken } from '@/access'; -import { PageEnum } from '@/enums/pagesEnums'; -import { getJobCompetitiveness } from '@/services/jobportal/competitiveness'; +import { ensureJobPortalLogin, PORTAL_LOGIN_URL } from '@/utils/jobPortalAuth'; +import { getDictLabel, findTreeLabelById } from '@/utils/jobPortalDict'; +import { getJobTitleTreeSelect } from '@/services/common/jobTitle'; +import { getCmsIndustryTreeList } from '@/services/classify/industry'; +import type { DictValueEnumObj } from '@/components/DictTag'; +import { + getJobCompetitiveness, + getEmptyCompetitivenessRadar, +} from '@/services/jobportal/competitiveness'; +import { isJobPortalLoggedIn } from '@/utils/jobPortalAuth'; import { Radar } from '@ant-design/charts'; import './index.less'; @@ -206,7 +212,9 @@ const JobListPage: React.FC = () => { const [jobList, setJobList] = useState([]); const [loading, setLoading] = useState(false); const [jobCategory, setJobCategory] = useState(''); - const [scaleEnum, setScaleEnum] = useState>({}); + const [scaleEnum, setScaleEnum] = useState({}); + const [jobTitleTree, setJobTitleTree] = useState([]); + const [industryTree, setIndustryTree] = useState([]); const [searchValue, setSearchValue] = useState(''); // 搜索框的值 const [filteredJobList, setFilteredJobList] = useState([]); // 过滤后的职位列表 const [overallScore, setOverallScore] = useState(0); @@ -230,13 +238,43 @@ const JobListPage: React.FC = () => { return '面议'; }; - // 获取字典数据 useEffect(() => { - getDictValueEnum('scale', true, true).then((data) => { - setScaleEnum(data); + Promise.all([ + getDictValueEnum('scale', true, true), + getCmsIndustryTreeList(), + getJobTitleTreeSelect(), + ]).then(([scaleData, industryRes, jobTitleRes]) => { + setScaleEnum(scaleData); + if (industryRes?.code === 200 && industryRes?.data) { + setIndustryTree(industryRes.data); + } + if (jobTitleRes?.code === 200 && jobTitleRes?.data) { + setJobTitleTree(jobTitleRes.data); + } }); }, []); + const selectedCompanyMetaItems = useMemo(() => { + if (!selectedJob) return []; + const company = selectedJob.companyVo || {}; + const rawScale = company.scale ?? selectedJob.scale; + const rawIndustry = company.industry ?? selectedJob.industry; + + const items: string[] = []; + const scaleLabel = getDictLabel(scaleEnum, rawScale) || rawScale; + const industryLabel = findTreeLabelById(industryTree, rawIndustry) || rawIndustry; + + if (scaleLabel) items.push(scaleLabel); + if (industryLabel) items.push(industryLabel); + return items; + }, [selectedJob, scaleEnum, industryTree]); + + const resolveJobCategoryLabel = (job: any): string => { + const raw = job?.jobCategory ?? job?.jobTitleId; + if (!raw) return ''; + return findTreeLabelById(jobTitleTree, raw) || raw; + }; + // 从路由queryParams获取jobCategory参数并获取数据 useEffect(() => { // 尝试从 state.queryParams 获取参数 @@ -289,10 +327,19 @@ const JobListPage: React.FC = () => { }, [selectedJob]); const fetchCompetitiveness = async (jobIdNum: number) => { + if (!isJobPortalLoggedIn()) { + setOverallScore(0); + setRadarData(getEmptyCompetitivenessRadar()); + return; + } try { const res = await getJobCompetitiveness(jobIdNum); const data = res as any; - if (!data) return; + if (!data) { + setOverallScore(0); + setRadarData(getEmptyCompetitivenessRadar()); + return; + } if (typeof data.matchScore === 'number') { const clamped = Math.max(0, Math.min(100, data.matchScore)); setOverallScore(clamped); @@ -311,17 +358,11 @@ const JobListPage: React.FC = () => { ].map(d => ({ ...d, score: Math.max(0, Math.min(100, d.score)) })); setRadarData(mapped); } else { - setRadarData([ - { item: '技能', score: 0 }, - { item: '工作经验', score: 0 }, - { item: '学历', score: 0 }, - { item: '薪资', score: 0 }, - { item: '年龄', score: 0 }, - { item: '工作地', score: 0 } - ]); + setRadarData(getEmptyCompetitivenessRadar()); } } catch (e) { - // 保持默认数据 + setOverallScore(0); + setRadarData(getEmptyCompetitivenessRadar()); } }; @@ -363,21 +404,15 @@ const JobListPage: React.FC = () => { }; const handleCollect = async () => { - // 检查用户是否登录 - const token = getAccessToken(); - if (!token) { - message.warning('请先登录'); - history.push(PageEnum.LOGIN); + if (!ensureJobPortalLogin('收藏职位')) { return; } - // 如果是取消收藏,调用取消收藏接口 if (favorited) { - // 获取用户ID const userId = getUserIdFromCache(); if (!userId) { message.warning('无法获取用户信息,请重新登录'); - history.push(PageEnum.LOGIN); + window.location.href = PORTAL_LOGIN_URL; return; } @@ -414,12 +449,10 @@ const JobListPage: React.FC = () => { return; } - // 如果是收藏,调用收藏接口 - // 获取用户ID const userId = getUserIdFromCache(); if (!userId) { message.warning('无法获取用户信息,请重新登录'); - history.push(PageEnum.LOGIN); + window.location.href = PORTAL_LOGIN_URL; return; } @@ -455,21 +488,15 @@ const JobListPage: React.FC = () => { } }; - // 处理立即申请 const handleApply = async () => { - // 检查用户是否登录 - const token = getAccessToken(); - if (!token) { - message.warning('请先登录'); - history.push(PageEnum.LOGIN); + if (!ensureJobPortalLogin('申请职位')) { return; } - // 获取用户ID const userId = getUserIdFromCache(); if (!userId) { message.warning('无法获取用户信息,请重新登录'); - history.push(PageEnum.LOGIN); + window.location.href = PORTAL_LOGIN_URL; return; } @@ -636,7 +663,7 @@ const JobListPage: React.FC = () => {
*/} {/* 主要内容区 */} -
+
{/* 返回按钮 */} {/*
{/* 添加标签区域,与热门岗位保持一致 */}
- {job.jobCategory && {job.jobCategory}} + {job.jobCategory && ( + {resolveJobCategoryLabel(job)} + )} {/* {job.dataSource && {job.dataSource}} */} {job.vacancies && 招聘{job.vacancies}人} {/* {job.industry && {job.industry}} */} @@ -742,114 +771,72 @@ const JobListPage: React.FC = () => {
) : ( <> - {/* 公司信息卡片 */} - {(selectedJob?.companyInfo || selectedJob?.companyName || selectedJob?.company) && ( - - 公司信息 -
- - 公司 - -
- - {selectedJob?.companyInfo?.name || selectedJob?.companyName || selectedJob?.company} - -
- {selectedJob?.industry || '暂无行业数据'} - - - -
-
- {selectedJob?.jobLocation || '暂无地址信息'} -
-
-
-
- - {selectedJob?.companyVo?.companyDescription || '暂无公司描述'} - -
-
- )} -
-
-
- - {selectedJob?.jobTitle || selectedJob?.title || '请选择职位'} - - {selectedJob?.minSalary && selectedJob?.maxSalary - ? formatSalary(selectedJob.minSalary, selectedJob.maxSalary) - : (selectedJob?.salary || '面议') - } - - +
+ + {selectedJob?.jobTitle || selectedJob?.title || '请选择职位'} + + + {selectedJob?.minSalary && selectedJob?.maxSalary + ? formatSalary(selectedJob.minSalary, selectedJob.maxSalary) + : (selectedJob?.salary || '面议')} + +
+ {(selectedJob?.jobLocation || selectedJob?.location) && ( + + {selectedJob?.jobLocation || selectedJob?.location} + + )} + + {selectedJob?.experience + ? experienceMap[selectedJob.experience] || selectedJob.experience + : '经验不限'} + + + {selectedJob?.education + ? educationMap[selectedJob.education] || selectedJob.education + : '学历不限'} + + {selectedJob?.jobCategory && ( + {resolveJobCategoryLabel(selectedJob)} + )} + {selectedJob?.vacancies && ( + 招聘{selectedJob.vacancies}人 + )} + {selectedJob?.tags?.map((tag: string, index: number) => ( + {tag} + ))} +
+
+
+ -
-
-
- + + + + + +
-
- - {selectedJob?.jobLocation || selectedJob?.location || '未知'} - {selectedJob?.experience ? experienceMap[selectedJob.experience] || selectedJob.experience : '不限'} - {selectedJob?.education ? educationMap[selectedJob.education] || selectedJob.education : '不限'} - - - - - -
- - + {/* 左侧内容区 */} - {/* 职位描述 */}
- 职位描述 -
- {selectedJob?.jobCategory && {selectedJob.jobCategory}} - {/* {selectedJob?.dataSource && {selectedJob.dataSource}} */} - {selectedJob?.industry && {selectedJob.industry}} - {selectedJob?.vacancies && 招聘{selectedJob.vacancies}人} - {selectedJob?.tags?.map((tag: string, index: number) => ( - {tag} - ))} -
- + 职位描述 + {selectedJob?.description || '暂无职位描述'}
@@ -919,6 +906,43 @@ const JobListPage: React.FC = () => {
+ + {/* 公司信息卡片 */} + {(selectedJob?.companyInfo || selectedJob?.companyName || selectedJob?.company) && ( + + 公司信息 +
+ + 公司 + +
+ + {selectedJob?.companyInfo?.name || selectedJob?.companyName || selectedJob?.company} + +
+ {selectedCompanyMetaItems.length > 0 ? ( + selectedCompanyMetaItems.map((label, index) => ( + + {index > 0 && } + {label} + + )) + ) : ( + 暂无公司信息 + )} +
+
+ {selectedJob?.jobLocation || '暂无地址信息'} +
+
+
+
+ + {selectedJob?.companyVo?.companyDescription || '暂无公司描述'} + +
+
+ )} )}
diff --git a/src/pages/JobPortal/Message/index.less b/src/pages/JobPortal/Message/index.less index c160b63..5047366 100644 --- a/src/pages/JobPortal/Message/index.less +++ b/src/pages/JobPortal/Message/index.less @@ -1,19 +1,19 @@ +@import '../theme.less'; + .message-page { min-height: 100vh; - background: #f5f5f5; + background: @jp-bg-page; .message-content { - padding: 24px; + padding: 24px 20px; .message-container { - max-width: 1200px; - margin: 0 auto; + .jp-page-container(); } .message-card { - background: #fff; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + .jp-card-base(); + padding: 24px; .message-header { display: flex; @@ -21,22 +21,19 @@ align-items: center; margin-bottom: 24px; padding-bottom: 16px; - border-bottom: 1px solid #f0f0f0; + border-bottom: 1px solid @jp-border; .page-title { + .jp-section-title(); margin: 0; - font-size: 24px; - font-weight: 600; - color: #262626; } } - .message-tabs { - margin-bottom: 16px; + .message-tabs .ant-tabs-tab { + font-size: 15px; - .ant-tabs-tab { - font-size: 16px; - padding: 12px 24px; + &.ant-tabs-tab-active .ant-tabs-tab-btn { + color: @jp-primary; } } @@ -44,245 +41,192 @@ margin-bottom: 24px; .category-card { - border-radius: 12px; - transition: all 0.3s; + .jp-card-base(); cursor: pointer; height: 100%; - border: 1px solid #f0f0f0; - background: #fafafa; + transition: all 0.2s; + + .ant-card-body { + padding: 20px 20px 18px; + } &:hover { - border-color: #d9d9d9; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); - transform: translateY(-2px); + border-color: @jp-primary-border; + box-shadow: @jp-shadow-hover; } &.active { - border-color: #1890ff; - background: #fff; - box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15); + border-color: @jp-primary; + background: @jp-primary-light; + box-shadow: @jp-shadow-hover; } .category-card-content { display: flex; + flex-direction: column; align-items: flex-start; gap: 16px; + } - .category-icon { - width: 48px; - height: 48px; - display: flex; - align-items: center; - justify-content: center; - font-size: 24px; - color: #fff; - flex-shrink: 0; + .category-icon { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + color: #fff; + flex-shrink: 0; + margin-bottom: 0; - &.job-icon { - background: #ff4d4f; - border-radius: 8px; - } - - &.appointment-icon { - background: #1890ff; - border-radius: 50%; - } - - &.system-icon { - background: #fa8c16; - border-radius: 50%; - } + &.job-icon { + background: @jp-primary; + border-radius: @jp-radius; } - .category-info { - flex: 1; - min-width: 0; - - .category-title { - margin: 0 0 8px 0; - font-size: 16px; - font-weight: 600; - color: #262626; - } - - .category-desc { - display: block; - font-size: 14px; - color: #8c8c8c; - line-height: 1.5; - margin-bottom: 8px; - } - - .category-date { - display: block; - font-size: 12px; - color: #bfbfbf; - text-align: right; - margin-top: 4px; - } + &.appointment-icon, + &.system-icon { + background: @jp-primary-dark; + border-radius: 50%; } } + + .category-info { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; + min-width: 0; + } + + .category-title { + margin: 0 !important; + color: @jp-text-primary; + font-weight: 600; + font-size: 16px !important; + line-height: 1.4; + } + + .category-desc { + display: block; + color: @jp-text-secondary; + font-size: 13px; + line-height: 1.6; + } + + .category-date { + display: block; + margin-top: 2px; + color: @jp-text-muted; + font-size: 12px; + } } } .category-filter-tip { - display: flex; - align-items: center; - gap: 12px; padding: 12px 16px; - background: #f0f7ff; - border-radius: 8px; - border-left: 4px solid #1890ff; + background: @jp-primary-light; + border-radius: @jp-radius; + border-left: 4px solid @jp-primary; } .message-item { - padding: 20px; - border-bottom: 1px solid #f0f0f0; + padding: 20px !important; + border-bottom: 1px solid @jp-border; cursor: pointer; - transition: all 0.3s; + transition: all 0.2s; border-left: 4px solid transparent; + align-items: flex-start !important; &:hover { - background: #fafafa; - border-left-color: #1890ff; + background: @jp-primary-light; + border-left-color: @jp-primary; } &.unread { - background: #f0f7ff; - border-left-color: #1890ff; + background: @jp-primary-light; + border-left-color: @jp-primary; .message-title { font-weight: 600; + color: @jp-text-primary; } } - &:last-child { - border-bottom: none; - } - .message-content-wrapper { display: flex; flex: 1; - gap: 16px; + align-items: flex-start; + gap: 14px; + min-width: 0; + } - .message-icon { - font-size: 32px; - flex-shrink: 0; - display: flex; - align-items: flex-start; - padding-top: 4px; + .message-icon { + flex-shrink: 0; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + background: @jp-primary-light; + border-radius: 50%; + border: 1px solid @jp-primary-border; + } + + .message-info { + flex: 1; + min-width: 0; + } + + .message-header-info { + margin-bottom: 6px; + + .ant-space { + flex-wrap: wrap; + gap: 8px 12px !important; } - .message-info { - flex: 1; - min-width: 0; - - .message-header-info { - margin-bottom: 8px; - - .message-title { - margin: 0; - font-size: 16px; - color: #262626; - display: inline-block; - } - } - - .message-text { - display: block; - color: #595959; - font-size: 14px; - line-height: 1.6; - margin-bottom: 12px; - word-break: break-word; - } - - .message-footer { - display: flex; - justify-content: space-between; - align-items: center; - - .message-time { - font-size: 12px; - color: #8c8c8c; - } - } + .message-title { + margin: 0 !important; + font-size: 15px !important; + line-height: 1.4; } } + .message-text { + display: block; + color: @jp-text-secondary; + font-size: 14px; + line-height: 1.6; + margin-bottom: 8px; + } + + .message-footer { + margin-top: 4px; + } + + .message-time { + color: @jp-text-muted; + font-size: 12px; + } + .message-actions { - display: flex; - gap: 8px; flex-shrink: 0; - margin-left: 16px; - } - } - } - } -} - -// 响应式设计 -@media (max-width: 768px) { - .message-page { - .message-content { - padding: 16px; - - .message-card { - .message-header { + display: flex; flex-direction: column; - align-items: flex-start; - gap: 16px; - - .page-title { - font-size: 20px; - } - } - - .message-category-cards { - .category-card { - margin-bottom: 12px; - - .category-card-content { - .category-icon { - width: 40px; - height: 40px; - font-size: 20px; - } - - .category-info { - .category-title { - font-size: 15px; - } - - .category-desc { - font-size: 13px; - } - } - } - } - } - - .message-item { - padding: 16px; - - .message-content-wrapper { - flex-direction: column; - gap: 12px; - - .message-icon { - font-size: 24px; - } - } - - .message-actions { - margin-left: 0; - margin-top: 12px; - width: 100%; - justify-content: flex-end; - } + align-items: flex-end; + gap: 4px; + margin-left: 12px; + padding-top: 4px; } } } } } +@media (max-width: 768px) { + .message-page .message-content { + padding: 16px 12px; + } +} diff --git a/src/pages/JobPortal/PersonalCenter/Applications/index.less b/src/pages/JobPortal/PersonalCenter/Applications/index.less index 91b3dff..c020524 100644 --- a/src/pages/JobPortal/PersonalCenter/Applications/index.less +++ b/src/pages/JobPortal/PersonalCenter/Applications/index.less @@ -1,130 +1,50 @@ +@import '../../theme.less'; + .applications-page { min-height: 100vh; - background-color: #f5f5f5; + background-color: @jp-bg-page; .page-content { - max-width: 1200px; - margin: 0 auto; - padding: 24px; + .jp-page-container(); + padding: 24px 20px 40px; - .back-button { + .back-button .ant-btn { + color: @jp-primary; + padding: 0; + + &:hover { + color: @jp-primary-dark; + } + } + + .page-header .ant-typography { + color: @jp-text-primary; + } + + .job-list .job-card { + .jp-card-base(); margin-bottom: 16px; + cursor: pointer; + transition: all 0.2s; - .ant-btn { - color: #1890ff; - padding: 0; - - &:hover { - color: #40a9ff; - } + &:hover { + border-color: @jp-primary-border; + box-shadow: @jp-shadow-hover; } - } - .page-header { - margin-bottom: 24px; - - .ant-typography { - margin-bottom: 8px; + .job-salary { + .jp-salary-text(); + font-size: 18px; } - } - .job-list { - .job-card { - margin-bottom: 16px; - border-radius: 8px; - cursor: pointer; - transition: all 0.3s; + .job-title { + color: @jp-text-primary; + font-weight: 600; + } - &:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - } - - .job-info { - .job-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 12px; - - .job-title { - margin: 0; - font-size: 18px; - color: #262626; - flex: 1; - } - - .job-salary { - color: #ff4d4f; - font-size: 18px; - font-weight: 600; - margin-left: 16px; - } - } - - .job-meta { - margin-bottom: 12px; - display: flex; - flex-wrap: wrap; - gap: 16px; - align-items: center; - - .job-company { - color: #595959; - font-size: 14px; - } - - .job-location { - color: #8c8c8c; - font-size: 14px; - - .anticon { - margin-right: 4px; - } - } - } - - .job-tags { - margin-bottom: 12px; - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: center; - - .job-tag { - margin: 0; - font-size: 13px; - font-weight: 500; - padding: 4px 12px; - border-radius: 4px; - border: none; - - &.experience-tag { - background-color: #e6f7ff; - color: #1890ff; - } - - &.vacancies-tag { - background-color: #fff7e6; - color: #fa8c16; - } - } - } - - .job-footer { - .anticon { - margin-right: 4px; - } - } - } - - .job-status { - display: flex; - justify-content: flex-end; - align-items: flex-start; - padding-top: 8px; - } + .job-tag.experience-tag { + .jp-job-tag(); } } } } - diff --git a/src/pages/JobPortal/PersonalCenter/Favorites/index.less b/src/pages/JobPortal/PersonalCenter/Favorites/index.less index 6d02208..5e653e5 100644 --- a/src/pages/JobPortal/PersonalCenter/Favorites/index.less +++ b/src/pages/JobPortal/PersonalCenter/Favorites/index.less @@ -1,128 +1,50 @@ +@import '../../theme.less'; + .favorites-page { min-height: 100vh; - background-color: #f5f5f5; + background-color: @jp-bg-page; .page-content { - max-width: 1200px; - margin: 0 auto; - padding: 24px; + .jp-page-container(); + padding: 24px 20px 40px; - .back-button { + .back-button .ant-btn { + color: @jp-primary; + padding: 0; + + &:hover { + color: @jp-primary-dark; + } + } + + .page-header .ant-typography { + color: @jp-text-primary; + } + + .job-list .job-card { + .jp-card-base(); margin-bottom: 16px; + cursor: pointer; + transition: all 0.2s; - .ant-btn { - color: #1890ff; - padding: 0; - - &:hover { - color: #40a9ff; - } + &:hover { + border-color: @jp-primary-border; + box-shadow: @jp-shadow-hover; } - } - .page-header { - margin-bottom: 24px; - - .ant-typography { - margin-bottom: 8px; + .job-salary { + .jp-salary-text(); + font-size: 18px; } - } - .job-list { - .job-card { - margin-bottom: 16px; - border-radius: 8px; - cursor: pointer; - transition: all 0.3s; + .job-title { + color: @jp-text-primary; + font-weight: 600; + } - &:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - } - - .job-info { - .job-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 12px; - - .job-title { - margin: 0; - font-size: 18px; - color: #262626; - flex: 1; - } - - .job-salary { - color: #ff4d4f; - font-size: 18px; - font-weight: 600; - margin-left: 16px; - } - } - - .job-meta { - margin-bottom: 12px; - display: flex; - flex-wrap: wrap; - gap: 16px; - align-items: center; - - .job-company { - color: #595959; - font-size: 14px; - } - - .job-location { - color: #8c8c8c; - font-size: 14px; - - .anticon { - margin-right: 4px; - } - } - } - - .job-tags { - margin-bottom: 12px; - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: center; - - .job-tag { - margin: 0; - font-size: 13px; - font-weight: 500; - padding: 4px 12px; - border-radius: 4px; - border: none; - - &.experience-tag { - background-color: #e6f7ff; - color: #1890ff; - } - - &.vacancies-tag { - background-color: #fff7e6; - color: #fa8c16; - } - } - } - - .job-footer { - margin-top: 8px; - } - } - - .job-actions { - display: flex; - justify-content: flex-end; - align-items: flex-start; - padding-top: 8px; - } + .job-tag.experience-tag { + .jp-job-tag(); } } } } - diff --git a/src/pages/JobPortal/PersonalCenter/Footprints/index.less b/src/pages/JobPortal/PersonalCenter/Footprints/index.less index 12d2739..af91b42 100644 --- a/src/pages/JobPortal/PersonalCenter/Footprints/index.less +++ b/src/pages/JobPortal/PersonalCenter/Footprints/index.less @@ -1,132 +1,50 @@ +@import '../../theme.less'; + .footprints-page { min-height: 100vh; - background-color: #f5f5f5; + background-color: @jp-bg-page; .page-content { - max-width: 1200px; - margin: 0 auto; - padding: 24px; + .jp-page-container(); + padding: 24px 20px 40px; - .back-button { + .back-button .ant-btn { + color: @jp-primary; + padding: 0; + + &:hover { + color: @jp-primary-dark; + } + } + + .page-header .ant-typography { + color: @jp-text-primary; + } + + .job-list .job-card { + .jp-card-base(); margin-bottom: 16px; + cursor: pointer; + transition: all 0.2s; - .ant-btn { - color: #1890ff; - padding: 0; - - &:hover { - color: #40a9ff; - } + &:hover { + border-color: @jp-primary-border; + box-shadow: @jp-shadow-hover; } - } - .page-header { - margin-bottom: 24px; - - .ant-typography { - margin-bottom: 8px; + .job-salary { + .jp-salary-text(); + font-size: 18px; } - } - .job-list { - .job-card { - margin-bottom: 16px; - border-radius: 8px; - cursor: pointer; - transition: all 0.3s; + .job-title { + color: @jp-text-primary; + font-weight: 600; + } - &:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - } - - .job-info { - .job-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 12px; - - .job-title { - margin: 0; - font-size: 18px; - color: #262626; - flex: 1; - } - - .job-salary { - color: #ff4d4f; - font-size: 18px; - font-weight: 600; - margin-left: 16px; - } - } - - .job-meta { - margin-bottom: 12px; - display: flex; - flex-wrap: wrap; - gap: 16px; - align-items: center; - - .job-company { - color: #595959; - font-size: 14px; - } - - .job-location { - color: #8c8c8c; - font-size: 14px; - - .anticon { - margin-right: 4px; - } - } - } - - .job-tags { - margin-bottom: 12px; - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: center; - - .job-tag { - margin: 0; - font-size: 13px; - font-weight: 500; - padding: 4px 12px; - border-radius: 4px; - border: none; - - &.experience-tag { - background-color: #e6f7ff; - color: #1890ff; - } - - &.vacancies-tag { - background-color: #fff7e6; - color: #fa8c16; - } - } - } - - .job-footer { - margin-top: 8px; - - .anticon { - margin-right: 4px; - } - } - } - - .job-visit-info { - display: flex; - justify-content: flex-end; - align-items: flex-start; - padding-top: 8px; - } + .job-tag.experience-tag { + .jp-job-tag(); } } } } - diff --git a/src/pages/JobPortal/PersonalCenter/index.less b/src/pages/JobPortal/PersonalCenter/index.less index 8cc771e..63c906f 100644 --- a/src/pages/JobPortal/PersonalCenter/index.less +++ b/src/pages/JobPortal/PersonalCenter/index.less @@ -1,200 +1,167 @@ +@import '../theme.less'; + .personal-center { min-height: 100vh; - background: #f5f5f5; - position: relative; - padding-bottom: 0; + background: @jp-bg-page; + padding-bottom: 40px; - // 页面最大宽度容器(PC) - .profile-content, - .user-header, - .statistics-section, - .resume-card, - .services-section, - .logout-section { - width: 1200px; - margin-left: auto; - margin-right: auto; + .center-wrapper { + .jp-page-container(); + margin-top: 20px; + padding: 0 20px; + box-sizing: border-box; } - // 移除移动端状态栏(PC不需要) + .profile-panel { + .jp-card-base(); + padding: 28px 32px 8px; + overflow: hidden; + } - // 用户信息头部 - .user-header { - background: #fff; - padding: 24px; + .profile-hero { display: flex; - justify-content: space-between; - align-items: center; + align-items: flex-start; + gap: 24px; - .user-info { - display: flex; - align-items: center; - gap: 12px; - - .user-avatar { - background: linear-gradient(135deg, #87CEEB, #98FB98); - border: 2px solid #fff; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - } - - .user-details { - .user-name { - margin: 0; - font-size: 20px; - font-weight: 600; - color: #000; - } - - .resume-status { - font-size: 12px; - color: #666; - margin-top: 4px; - display: block; - } - } + .user-avatar { + flex-shrink: 0; + border: 3px solid @jp-primary-light; + box-shadow: @jp-shadow-card; } - .user-actions { - .action-btn { - background: #fff; - border: 1px solid #e8e8e8; - border-radius: 20px; - padding: 8px 12px; - height: auto; + .profile-main { + flex: 1; + min-width: 0; + + .user-name { + margin: 0 0 10px; + font-size: 22px; + font-weight: 600; + color: @jp-text-primary; + } + + .profile-tags { display: flex; + flex-wrap: wrap; align-items: center; gap: 8px; + margin-bottom: 16px; - .btn-content { + .edu-tag { + .jp-job-tag(); + margin: 0; + } + } + + .resume-progress { + max-width: 360px; + + .progress-label { display: flex; align-items: center; - gap: 4px; + gap: 6px; + margin-bottom: 8px; + font-size: 13px; - .dots { - font-size: 16px; - color: #666; + .progress-value { + color: @jp-primary; + } + + .progress-tip { + font-size: 13px; } } + } + } - .arrow { - font-size: 12px; - color: #999; - } + .edit-resume-btn { + flex-shrink: 0; + align-self: center; + height: 40px; + padding: 0 24px; + border-radius: @jp-radius; + background: @jp-primary; + border-color: @jp-primary; + box-shadow: 0 2px 8px rgba(24, 144, 255, 0.25); - &:hover { - border-color: #1890ff; - } + &:hover { + background: @jp-primary-dark; + border-color: @jp-primary-dark; } } } - // 统计数据 - .statistics-section { - background: #fff; - padding: 24px; - margin-bottom: 16px; + .panel-divider { + margin: 24px 0; + border-color: @jp-border; + } + .stats-section { .stat-item { text-align: center; cursor: pointer; - transition: all 0.3s; - padding: 8px; - border-radius: 8px; + padding: 8px 12px; + border-right: 1px solid @jp-border; + transition: background 0.2s; + + &.stat-item-last { + border-right: none; + } &:hover { - background-color: #f0f0f0; + background: @jp-primary-light; + border-radius: @jp-radius; } .stat-number { - font-size: 24px; + font-size: 28px; font-weight: 600; - color: #000; - margin-bottom: 4px; + color: @jp-primary; + line-height: 1.2; } .stat-label { - font-size: 12px; - color: #666; + margin-top: 6px; + font-size: 13px; + color: @jp-text-muted; } } } - // 简历信息卡片 - .resume-card { - margin: 0 auto 16px; - border-radius: 12px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border: none; - box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); - - .ant-card-body { - padding: 20px; - } - - .resume-content { - display: flex; - justify-content: space-between; - align-items: center; - - .resume-info { - .resume-title { - color: #fff; - font-size: 16px; - font-weight: 600; - margin-bottom: 4px; - } - - .resume-position { - color: rgba(255, 255, 255, 0.9); - font-size: 14px; - } - } - - .edit-resume-btn { - background: rgba(255, 255, 255, 0.2); - border: 1px solid rgba(255, 255, 255, 0.3); - color: #fff; - border-radius: 20px; - padding: 8px 16px; - height: auto; - - &:hover { - background: rgba(255, 255, 255, 0.3); - border-color: rgba(255, 255, 255, 0.5); - } - } - } - } - - // 服务专区 .services-section { - background: #fff; - margin: 0 auto 16px; - border-radius: 12px; - padding: 24px; + padding-bottom: 16px; .section-title { - margin: 0 0 16px 0; + margin-bottom: 16px; font-size: 16px; font-weight: 600; - color: #000; + color: @jp-text-primary; + padding-left: 12px; + border-left: 4px solid @jp-primary; + line-height: 1.2; + } + + .ant-list-item { + padding: 0; + border: none; } .service-item { - padding: 16px 0; - border-bottom: 1px solid #f0f0f0; + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 12px; + margin-bottom: 4px; + border-radius: @jp-radius; cursor: pointer; - transition: background-color 0.2s; + transition: background 0.2s; &:last-child { - border-bottom: none; + margin-bottom: 0; } &:hover { - background-color: #fafafa; - border-radius: 8px; - margin: 0 -8px; - padding: 16px 8px; + background: @jp-primary-light; } .service-content { @@ -203,95 +170,72 @@ gap: 12px; .service-icon { - font-size: 20px; - width: 24px; - text-align: center; + font-size: 22px; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: @jp-bg-page; + border-radius: @jp-radius; } .service-info { - flex: 1; + display: flex; + flex-direction: column; + gap: 2px; .service-title { - font-size: 14px; - color: #000; - display: block; - margin-bottom: 2px; + font-size: 15px; + color: @jp-text-primary; + font-weight: 500; } .service-status { - font-size: 12px; - color: #52c41a; + font-size: 13px; } } } .service-arrow { - color: #ccc; + color: @jp-text-muted; font-size: 12px; } } } - - // 退出登录 - .logout-section { - text-align: center; - margin: 16px auto; - - .logout-btn { - color: #1890ff; - font-size: 16px; - font-weight: 500; - padding: 12px 24px; - - &:hover { - color: #40a9ff; - } - } - } - - // 移除底部导航栏(PC不需要) } -// 响应式设计 @media (max-width: 768px) { .personal-center { - .user-header { - padding: 16px; + .center-wrapper { + padding: 0 12px; + margin-top: 12px; + } - .user-info { - .user-details { - .user-name { - font-size: 18px; - } - } + .profile-panel { + padding: 20px 16px 4px; + } + + .profile-hero { + flex-direction: column; + align-items: stretch; + + .edit-resume-btn { + align-self: stretch; + width: 100%; + } + + .profile-main .resume-progress { + max-width: none; } } - .statistics-section { - padding: 16px; + .stats-section .stat-item { + padding: 12px 8px; - .stat-item { - .stat-number { - font-size: 20px; - } + .stat-number { + font-size: 22px; } } - - .resume-card { - margin: 0 12px 12px; - - .ant-card-body { - padding: 16px; - } - } - - .services-section { - margin: 0 12px 12px; - padding: 16px 12px; - } - - .logout-section { - margin: 12px; - } } } diff --git a/src/pages/JobPortal/PersonalCenter/index.tsx b/src/pages/JobPortal/PersonalCenter/index.tsx index 6d5e237..90ff15f 100644 --- a/src/pages/JobPortal/PersonalCenter/index.tsx +++ b/src/pages/JobPortal/PersonalCenter/index.tsx @@ -1,31 +1,22 @@ import React, { useState, useEffect, useMemo } from 'react'; import { - Card, Avatar, Typography, Button, - Space, Row, Col, List, Progress, - message, - Spin + Tag, + Spin, + Divider } from 'antd'; import { UserOutlined, - EditOutlined, - EyeOutlined, RightOutlined, - HeartOutlined, SafetyCertificateOutlined, - ClockCircleOutlined, - RobotOutlined, BellOutlined, - HomeOutlined, - CalendarOutlined as JobFairOutlined, - MessageOutlined, - UserOutlined as MyOutlined + FileTextOutlined } from '@ant-design/icons'; import { history } from '@umijs/max'; import JobPortalHeader from '@/components/JobPortalHeader'; @@ -37,26 +28,21 @@ import './index.less'; const { Title, Text } = Typography; -// 计算简历完成度 const calculateResumeCompleteness = (userInfo: API.AppUserInfo | null): number => { if (!userInfo) return 0; let completedFields = 0; const totalFields = 10; - // 基本信息 if (userInfo.name) completedFields++; if (userInfo.sex) completedFields++; if (userInfo.education) completedFields++; if (userInfo.phone) completedFields++; if (userInfo.avatar) completedFields++; - // 工作期望 if (userInfo.salaryMin && userInfo.salaryMax) completedFields++; if (userInfo.area) completedFields++; if (userInfo.jobTitleId) completedFields++; - // 工作经历 if (userInfo.experiencesList && userInfo.experiencesList.length > 0) completedFields++; - // 技能 if (userInfo.appSkillsList && userInfo.appSkillsList.length > 0) completedFields++; return Math.round((completedFields / totalFields) * 100); @@ -71,7 +57,6 @@ const PersonalCenter: React.FC = () => { age?: Record; }>({}); - // 获取字典数据 useEffect(() => { Promise.all([ getDictValueEnum('sex', false, true), @@ -88,26 +73,51 @@ const PersonalCenter: React.FC = () => { }); }, []); - // 获取用户信息 useEffect(() => { fetchUserInfo(); }, [fetchUserInfo]); - // 获取字典文本 - const getDictText = (type: 'sex' | 'education' | 'area' | 'age', value: string | undefined): string => { - if (!value) return ''; + const getDictText = ( + type: 'sex' | 'education' | 'area' | 'age', + value: string | number | null | undefined, + ): string => { + if (value === null || value === undefined || value === '') return ''; const dict = dictData[type]; - if (dict && dict[value]) { - return dict[value].text || dict[value].label || value; + if (!dict) return String(value); + + const strVal = String(value); + const direct = dict[strVal] ?? dict[Number(value)]; + if (direct?.text) return direct.text; + if (direct?.label) return direct.label; + + const matched = Object.entries(dict).find(([dictKey, item]) => { + const entry = item as { value?: string | number; key?: string | number }; + return dictKey === strVal || String(entry.value) === strVal || String(entry.key) === strVal; + }); + if (matched) { + const [, item] = matched; + return (item as { text?: string; label?: string }).text + || (item as { text?: string; label?: string }).label + || strVal; } - return value; + return strVal; + }; + + const getEducationDisplay = (): string => { + const text = getDictText('education', userInfo?.education); + return text || userInfo?.education || '学历未填写'; + }; + + const getAgeDisplay = (): string => { + if (!userInfo?.age) return '年龄未填写'; + const text = getDictText('age', userInfo.age) || userInfo.age; + return text.includes('岁') ? text : `${text}岁`; }; - // 获取简历完成度 const resumeCompleteness = calculateResumeCompleteness(userInfo); const resumeStatus = resumeCompleteness >= 80 ? '已完成' : resumeCompleteness >= 60 ? '建议优化' : '待完善'; + const progressStatus = resumeCompleteness >= 80 ? 'success' : resumeCompleteness >= 60 ? 'normal' : 'exception'; - // 获取最新的工作经历作为职位 const getLatestPosition = (): string => { if (userInfo?.experiencesList && userInfo.experiencesList.length > 0) { const sorted = [...userInfo.experiencesList].sort((a, b) => { @@ -127,7 +137,6 @@ const PersonalCenter: React.FC = () => { appointments: 0 }); - // 获取统计数据 useEffect(() => { const fetchStatistics = async () => { try { @@ -147,37 +156,19 @@ const PersonalCenter: React.FC = () => { fetchStatistics(); }, []); - // 判断是否实名认证:根据 userInfo 的 idCard 字段 const isRealNameVerified = useMemo(() => { return !!(userInfo?.idCard && userInfo.idCard.trim()); }, [userInfo?.idCard]); - // 服务列表,根据用户信息动态生成 const services = useMemo(() => [ { id: 1, title: '实名认证', icon: , - status: isRealNameVerified ? '已认证' : '', + status: isRealNameVerified ? '已认证' : '未认证', hasArrow: !isRealNameVerified, color: isRealNameVerified ? '#52c41a' : '#8c8c8c' }, - { - id: 2, - title: '素质测评', - icon: , - status: '', - hasArrow: true, - color: '#1890ff' - }, - { - id: 3, - title: 'AI面试', - icon: , - status: '', - hasArrow: true, - color: '#722ed1' - }, { id: 4, title: '通知与提醒', @@ -188,144 +179,133 @@ const PersonalCenter: React.FC = () => { } ], [isRealNameVerified]); - const handleEditResume = () => { - message.info('跳转到简历编辑页面'); - // history.push('/job-portal/resume/edit'); - }; - const handleServiceClick = (service: any) => { if (service.hasArrow) { - message.info(`跳转到${service.title}页面`); - // 根据服务类型跳转到不同页面 - switch (service.id) { - case 2: - // history.push('/job-portal/assessment'); - break; - case 3: - // history.push('/job-portal/ai-interview'); - break; - default: - break; - } + // 预留跳转 } }; - - const handleNavClick = (path: string) => { - history.push(path); - }; - return (
- {/* 用户信息头部 */} -
-
- } - className="user-avatar" - /> -
- {userInfo?.name || '未登录'} - - 简历完成度{resumeCompleteness}%, {resumeStatus} - -
-
- {/*
- -
*/} -
- - {/* 统计数据 */} -
- - -
history.push('/job-portal/personal-center/applications')}> -
{statistics.applications}
-
投递
-
- - -
history.push('/job-portal/personal-center/favorites')}> -
{statistics.favorites}
-
收藏
-
- - -
history.push('/job-portal/personal-center/footprints')}> -
{statistics.footprints}
-
足迹
-
- - -
-
{statistics.appointments}
-
预约
-
- -
-
- - {/* 简历信息卡片 */} - -
-
-
- - {userInfo?.name || '未登录'} | {getDictText('education', userInfo?.education) || '未填写'} - -
-
- {getLatestPosition()} -
-
- -
-
- - {/* 服务专区 */} -
- 服务专区 - ( - handleServiceClick(service)} - > -
-
- {service.icon} +
+
+
+ } + className="user-avatar" + /> +
+ {userInfo?.name || '未登录'} +
+ {getEducationDisplay()} + {getAgeDisplay()} + {getLatestPosition()}
-
- {service.title} - {service.status && ( - {service.status} - )} +
+
+ 简历完成度 + {resumeCompleteness}% + · {resumeStatus} +
+
- {service.hasArrow && } - - )} - /> -
+ +
+ + + +
+ + +
history.push('/job-portal/personal-center/applications')} + > +
{statistics.applications}
+
投递
+
+ + +
history.push('/job-portal/personal-center/favorites')} + > +
{statistics.favorites}
+
收藏
+
+ + +
history.push('/job-portal/personal-center/footprints')} + > +
{statistics.footprints}
+
足迹
+
+ + +
+
{statistics.appointments}
+
预约
+
+ +
+
+ + + +
+
服务专区
+ ( + handleServiceClick(service)} + > +
+
+ {service.icon} +
+
+ {service.title} + {service.status && ( + + {service.status} + + )} +
+
+ {service.hasArrow && } +
+ )} + /> +
+
+
); diff --git a/src/pages/JobPortal/Policy/Detail/index.less b/src/pages/JobPortal/Policy/Detail/index.less new file mode 100644 index 0000000..9477437 --- /dev/null +++ b/src/pages/JobPortal/Policy/Detail/index.less @@ -0,0 +1,121 @@ +@import '../../theme.less'; + +.policy-detail-page { + min-height: 100vh; + background: @jp-bg-page; + + .detail-content { + padding: 24px 20px 40px; + + .detail-container { + .jp-page-container(); + } + + .back-button { + padding: 0; + margin-bottom: 16px; + color: @jp-primary; + + &:hover { + color: @jp-primary-dark; + } + } + + .detail-card { + .jp-card-base(); + padding: 28px 32px; + } + + .detail-title { + margin: 0 0 16px; + color: @jp-text-primary; + line-height: 1.5; + } + + .detail-meta-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; + + .type-tag { + .jp-job-tag(); + } + + .level-tag { + background: #f6ffed !important; + border: 1px solid #b7eb8f !important; + color: #52c41a !important; + } + } + + .detail-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 20px; + } + + .detail-summary { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 24px; + font-size: 14px; + + .summary-source { + line-height: 1.6; + } + + .anticon { + margin-right: 6px; + } + } + + .policy-article { + margin-bottom: 28px; + padding: 24px; + background: #fafbfc; + border: 1px solid @jp-border; + border-radius: @jp-radius; + + .policy-content { + margin: 0; + font-family: inherit; + font-size: 15px; + line-height: 1.9; + color: @jp-text-primary; + white-space: pre-wrap; + word-break: break-word; + background: transparent; + border: none; + } + } + + .content-empty { + margin: 32px 0; + } + + .detail-desc { + margin-bottom: 24px; + } + + .extra-block { + margin-top: 24px; + padding-top: 20px; + border-top: 1px dashed @jp-border; + + h5 { + margin-bottom: 12px; + color: @jp-text-primary; + } + + .extra-text { + margin: 0; + color: @jp-text-secondary; + white-space: pre-wrap; + line-height: 1.8; + } + } + } +} diff --git a/src/pages/JobPortal/Policy/Detail/index.tsx b/src/pages/JobPortal/Policy/Detail/index.tsx new file mode 100644 index 0000000..d326f18 --- /dev/null +++ b/src/pages/JobPortal/Policy/Detail/index.tsx @@ -0,0 +1,191 @@ +import React, { useEffect, useState } from 'react'; +import { + Button, + Descriptions, + Spin, + Tag, + Typography, + message, + Empty, +} from 'antd'; +import { + ArrowLeftOutlined, + CalendarOutlined, + EyeOutlined, + FileTextOutlined, + BankOutlined, +} from '@ant-design/icons'; +import { history, useSearchParams } from '@umijs/max'; +import JobPortalHeader from '@/components/JobPortalHeader'; +import { getPolicyInfoDetail } from '@/services/cms/policyInfo'; +import { getDictValueEnum } from '@/services/system/dict'; +import DictTag, { DictValueEnumObj } from '@/components/DictTag'; +import './index.less'; + +const { Title, Paragraph, Text } = Typography; + +/** 解析 GET /cms/policyInfo/{id} 响应 */ +const parsePolicyDetail = ( + response: API.PolicyInfo.PolicyInfoDetailResult | undefined, +): API.PolicyInfo.PolicyInfoItem | null => { + if (!response) return null; + if (response.code === 200 && response.data) { + return response.data; + } + return null; +}; + +const PolicyDetailPage: React.FC = () => { + const [searchParams] = useSearchParams(); + const policyId = Number(searchParams.get('id')); + const [detail, setDetail] = useState(null); + const [loading, setLoading] = useState(false); + const [userTypeEnum, setUserTypeEnum] = useState({}); + + useEffect(() => { + getDictValueEnum('user_type', false, true).then(setUserTypeEnum); + }, []); + + useEffect(() => { + if (!policyId || Number.isNaN(policyId)) { + return; + } + + const fetchDetail = async () => { + try { + setLoading(true); + setDetail(null); + const response = await getPolicyInfoDetail(policyId); + const policy = parsePolicyDetail(response); + if (policy) { + setDetail(policy); + } else { + message.error(response?.msg || '获取政策详情失败'); + } + } catch (error) { + console.error('获取政策详情失败:', error); + message.error('获取政策详情失败'); + } finally { + setLoading(false); + } + }; + + fetchDetail(); + }, [policyId]); + + const renderPolicyTags = () => { + if (!detail?.policyTag) return null; + const tags = String(detail.policyTag).split(',').filter(Boolean); + return ( +
+ {tags.map((tag) => ( + + ))} +
+ ); + }; + + const renderExtraBlock = (label: string, content?: string | null) => { + if (!content?.trim()) return null; + return ( +
+ {label} + {content} +
+ ); + }; + + const invalidId = !policyId || Number.isNaN(policyId); + + return ( +
+ + +
+
+ + + + {invalidId ? ( + + + + ) : detail ? ( +
+ {detail.zcmc} + +
+ {detail.zclx && {detail.zclx}} + {detail.zcLevel && {detail.zcLevel}} +
+ {renderPolicyTags()} + +
+ {detail.publishTime && ( + + 发文时间:{detail.publishTime} + + )} + {detail.sourceUnit && ( + + {detail.sourceUnit} + + )} + + {detail.viewNum ?? 0} 次浏览 + +
+ + {detail.zcContent?.trim() ? ( +
+
{detail.zcContent}
+
+ ) : ( + + )} + + {(detail.acceptUnit || detail.fileUrl) && ( + + {detail.acceptUnit && ( + {detail.acceptUnit} + )} + {detail.fileUrl && ( + + + {detail.fileName || '查看附件'} + + + )} + + )} + + {renderExtraBlock('补贴标准', detail.subsidyStandard)} + {renderExtraBlock('经办渠道', detail.handleChannel)} + {renderExtraBlock('申报条件', detail.applyCondition)} +
+ ) : ( + !loading && ( + + + + ) + )} +
+
+
+
+ ); +}; + +export default PolicyDetailPage; diff --git a/src/pages/JobPortal/Policy/index.less b/src/pages/JobPortal/Policy/index.less new file mode 100644 index 0000000..d8aa99a --- /dev/null +++ b/src/pages/JobPortal/Policy/index.less @@ -0,0 +1,129 @@ +@import '../theme.less'; + +.policy-page { + min-height: 100vh; + background: @jp-bg-page; + + .policy-content { + padding: 24px 20px 40px; + + .policy-container { + .jp-page-container(); + } + + .policy-card { + .jp-card-base(); + padding: 24px 28px 16px; + } + + .policy-header { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid @jp-border; + + .page-title { + .jp-section-title(); + margin: 0; + + .anticon { + margin-right: 8px; + color: @jp-primary; + } + } + + .policy-search { + width: 320px; + max-width: 100%; + + .search-icon { + color: @jp-text-muted; + cursor: pointer; + + &:hover { + color: @jp-primary; + } + } + } + } + + .policy-list { + .policy-item { + padding: 20px 0; + cursor: pointer; + border-bottom: 1px solid @jp-border; + transition: background 0.2s; + + &:hover { + background: @jp-primary-light; + margin: 0 -12px; + padding-left: 12px; + padding-right: 12px; + border-radius: @jp-radius; + } + + &:last-child { + border-bottom: none; + } + } + + .policy-title { + margin: 0 0 10px; + color: @jp-text-primary; + font-weight: 600; + line-height: 1.5; + } + + .policy-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 8px; + + .type-tag { + .jp-job-tag(); + } + + .level-tag { + background: #f6ffed !important; + border: 1px solid #b7eb8f !important; + color: #52c41a !important; + } + } + + .policy-source { + display: block; + margin-bottom: 8px; + font-size: 13px; + + .anticon { + margin-right: 4px; + } + } + + .policy-meta { + display: flex; + flex-wrap: wrap; + gap: 20px; + font-size: 13px; + color: @jp-text-muted; + + .anticon { + margin-right: 4px; + } + } + } + + .policy-pagination { + display: flex; + justify-content: center; + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid @jp-border; + } + } +} diff --git a/src/pages/JobPortal/Policy/index.tsx b/src/pages/JobPortal/Policy/index.tsx new file mode 100644 index 0000000..a2beda8 --- /dev/null +++ b/src/pages/JobPortal/Policy/index.tsx @@ -0,0 +1,158 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { + Input, + List, + Pagination, + Spin, + Tag, + Typography, + Empty, + message, +} from 'antd'; +import { + CalendarOutlined, + EyeOutlined, + FileTextOutlined, + SearchOutlined, + BankOutlined, +} from '@ant-design/icons'; +import { history } from '@umijs/max'; +import JobPortalHeader from '@/components/JobPortalHeader'; +import { getPolicyInfoList } from '@/services/cms/policyInfo'; +import './index.less'; + +const { Title, Text } = Typography; + +const PolicyListPage: React.FC = () => { + const [list, setList] = useState([]); + const [loading, setLoading] = useState(false); + const [total, setTotal] = useState(0); + const [pageNum, setPageNum] = useState(1); + const [pageSize] = useState(10); + const [searchValue, setSearchValue] = useState(''); + const [keyword, setKeyword] = useState(''); + + const fetchList = useCallback(async () => { + try { + setLoading(true); + const response = await getPolicyInfoList({ + pageNum, + pageSize, + searchValue: keyword || undefined, + }); + if (response?.code === 200) { + setList(response.rows || []); + setTotal(response.total || 0); + } else { + message.error(response?.msg || '获取政策列表失败'); + } + } catch (error) { + console.error('获取政策列表失败:', error); + message.error('获取政策列表失败'); + } finally { + setLoading(false); + } + }, [pageNum, pageSize, keyword]); + + useEffect(() => { + fetchList(); + }, [fetchList]); + + const handleSearch = () => { + setPageNum(1); + setKeyword(searchValue.trim()); + }; + + const handleItemClick = (item: API.PolicyInfo.PolicyInfoItem) => { + history.push(`/job-portal/policy/detail?id=${item.id}`); + }; + + return ( +
+ + +
+
+
+
+ + <FileTextOutlined /> 政策资讯 + +
+ setSearchValue(e.target.value)} + onPressEnter={handleSearch} + allowClear + suffix={ + + } + /> +
+
+ + + {list.length > 0 ? ( + <> + ( + handleItemClick(item)} + > +
+ + {item.zcmc} + +
+ {item.zclx && {item.zclx}} + {item.zcLevel && {item.zcLevel}} +
+ {item.sourceUnit && ( + + {item.sourceUnit} + + )} +
+ {item.publishTime && ( + + {item.publishTime} + + )} + + {item.viewNum ?? 0} 次浏览 + +
+
+
+ )} + /> +
+ `共 ${t} 条`} + onChange={(page) => setPageNum(page)} + /> +
+ + ) : ( + !loading && + )} +
+
+
+
+
+ ); +}; + +export default PolicyListPage; diff --git a/src/pages/JobPortal/Profile/index.less b/src/pages/JobPortal/Profile/index.less index 46cb5a9..5414709 100644 --- a/src/pages/JobPortal/Profile/index.less +++ b/src/pages/JobPortal/Profile/index.less @@ -1,137 +1,59 @@ +@import '../theme.less'; + .job-portal-profile { min-height: 100vh; - background-color: #f5f5f5; + background-color: @jp-bg-page; .profile-content { padding: 24px 0; .profile-container { - max-width: 1200px; - margin: 0 auto; - padding: 0 20px; + .jp-page-container(); + + .profile-card, + .section-card { + .jp-card-base(); + margin-bottom: 16px; + } .profile-card { - .avatar-section { - text-align: center; - - .edit-avatar-btn { - margin-top: 16px; - width: 100%; - } + .user-info .user-name { + color: @jp-primary; } - .user-info { - .user-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; + .detail-icon, + .ant-progress-bg { + color: @jp-primary; + } - .user-name { - margin: 0; - color: #1890ff; - } - } - - .user-details { - margin-bottom: 16px; - - .detail-item { - display: flex; - align-items: center; - gap: 8px; - color: #666; - - .detail-icon { - color: #1890ff; - } - } - } - - .contact-info { - margin-bottom: 16px; - - .contact-item { - display: flex; - align-items: center; - gap: 8px; - color: #999; - - .contact-icon { - color: #666; - } - } - } - - .profile-completeness { - .ant-progress { - .ant-progress-bg { - background: linear-gradient(90deg, #1890ff 0%, #40a9ff 100%); - } - } - } + .ant-progress .ant-progress-bg { + background: @jp-primary; } } .section-card { - .ant-card-head { - border-bottom: 1px solid #f0f0f0; - - .ant-card-head-title { - color: #1890ff; - font-weight: 600; - } + .ant-card-head-title { + color: @jp-primary; + font-weight: 600; } - .resume-section { - .resume-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 16px; + .stats-grid .stat-item { + background: @jp-primary-light; + border: 1px solid @jp-primary-border; + border-radius: @jp-radius; + transition: all 0.2s; - .resume-info { - .ant-typography { - margin-bottom: 4px; - } - } + &:hover { + border-color: @jp-primary; + box-shadow: @jp-shadow-hover; } - .resume-status { - display: flex; - align-items: center; - gap: 16px; + .stat-number { + color: @jp-primary; } - } - .stats-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 16px; - - .stat-item { - text-align: center; - padding: 16px; - background: #f8f9fa; - border-radius: 8px; - transition: all 0.3s; - - &:hover { - background: #e6f7ff; - transform: translateY(-2px); - } - - .stat-number { - font-size: 24px; - font-weight: bold; - color: #1890ff; - margin-bottom: 4px; - } - - .stat-label { - font-size: 12px; - color: #666; - } + .stat-label { + color: @jp-text-muted; } } } @@ -139,63 +61,8 @@ } } -// 响应式设计 @media (max-width: 768px) { - .job-portal-profile { - .profile-content { - padding: 16px 0; - - .profile-container { - padding: 0 16px; - - .profile-card { - .ant-row { - .ant-col { - margin-bottom: 16px; - } - } - - .user-info { - .user-header { - flex-direction: column; - align-items: flex-start; - gap: 12px; - - .user-name { - font-size: 20px; - } - } - - .user-details { - .ant-space { - width: 100%; - justify-content: flex-start; - } - } - - .contact-info { - .ant-space { - flex-direction: column; - align-items: flex-start; - width: 100%; - } - } - } - } - - .ant-row { - .ant-col { - margin-bottom: 16px; - } - } - - .section-card { - .stats-grid { - grid-template-columns: 1fr; - gap: 12px; - } - } - } - } + .job-portal-profile .profile-content .profile-container { + padding: 0 16px; } } diff --git a/src/pages/JobPortal/Resume/index.less b/src/pages/JobPortal/Resume/index.less index 321fa7b..ba7bb27 100644 --- a/src/pages/JobPortal/Resume/index.less +++ b/src/pages/JobPortal/Resume/index.less @@ -1,38 +1,42 @@ +@import '../theme.less'; + .resume-page { - background: #f5f5f5; + background: @jp-bg-page; min-height: 100vh; .container { - width: 1200px; - margin: 0 auto; - padding: 16px 0 32px; + .jp-page-container(); + padding: 20px 20px 40px; } .section { - background: #fff; - border-radius: 8px; + .jp-card-base(); margin-bottom: 16px; - } + padding: 0; - .card-profile { - .name { - margin: 0; - } - } + .ant-card-head { + border-bottom: 1px solid @jp-border; - .exp-item { - .duties { - .duty-line { - line-height: 22px; + .ant-card-head-title { + color: @jp-primary; + font-weight: 600; } } } + + .card-profile .name { + color: @jp-text-primary; + font-weight: 600; + } + + .ant-btn-primary { + background: @jp-primary; + border-color: @jp-primary; + } } @media (max-width: 1366px) { - .resume-page { - .container { width: 1040px; } + .resume-page .container { + max-width: 1040px; } } - - diff --git a/src/pages/JobPortal/Resume/index.tsx b/src/pages/JobPortal/Resume/index.tsx index 9678c12..e5bf049 100644 --- a/src/pages/JobPortal/Resume/index.tsx +++ b/src/pages/JobPortal/Resume/index.tsx @@ -112,11 +112,30 @@ const ResumePage: React.FC = () => { const [skillOptions, setSkillOptions] = React.useState>([]); const [skillSearchLoading, setSkillSearchLoading] = React.useState(false); - // 获取字典值的显示文本 - const getDictText = (dictType: string, value: string | null | undefined): string => { - if (!value || !dictData[dictType as keyof typeof dictData]) return value || ''; + // 获取字典值的显示文本(支持 dictValue / dictCode 多种存储格式) + const getDictText = (dictType: string, value: string | number | null | undefined): string => { + if (value === null || value === undefined || value === '') return ''; const dict = dictData[dictType as keyof typeof dictData]; - return dict[value]?.text || value; + if (!dict) return String(value); + + const strVal = String(value); + const direct = dict[strVal] ?? dict[Number(value)]; + if (direct?.text) return direct.text; + if (direct?.label) return direct.label; + + const matched = Object.entries(dict).find(([dictKey, item]) => { + const entry = item as { value?: string | number; key?: string | number; text?: string; label?: string }; + return ( + dictKey === strVal || + String(entry.value) === strVal || + String(entry.key) === strVal + ); + }); + if (matched) { + const [, item] = matched; + return item.text || item.label || strVal; + } + return strVal; }; // 在树形数据中递归查找id对应的label @@ -750,7 +769,7 @@ const ResumePage: React.FC = () => { 期望工作地: - {getDictText('area', profile.desiredCity) || profile.desiredCity || '未填写'} + {getDictText('area', profile.desiredCity) || '未填写'} diff --git a/src/pages/JobPortal/index.less b/src/pages/JobPortal/index.less index 2012579..6b39caa 100644 --- a/src/pages/JobPortal/index.less +++ b/src/pages/JobPortal/index.less @@ -1,31 +1,60 @@ +@import './theme.less'; + .job-portal { min-height: 100vh; - background-color: #f5f5f5; + background-color: @jp-bg-page; - - // 主要内容区域 .main-content { - max-width: 1200px; - margin: 0 auto; - padding: 0 20px; + .jp-page-container(); margin-top: 20px; - .ant-row { - margin: 0 !important; + padding-bottom: 48px; + + .category-layout { + margin-bottom: 24px; + + .ant-col { + padding: 0 8px !important; + } } - .ant-col { - padding: 0 8px !important; // 减少列间距 + .panel-header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; + flex-wrap: wrap; + + .panel-title { + margin: 0; + font-size: 16px; + font-weight: 600; + color: @jp-text-primary; + } + + .panel-tip { + font-size: 13px; + } } - // 左侧行业面板 .industry-panel { - height: 600px; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + .jp-card-base(); + height: 420px; + min-height: 420px; + display: flex; + flex-direction: column; + + .panel-header { + padding: 16px 20px 0; + margin-bottom: 8px; + flex-shrink: 0; + } .industry-list { - padding: 16px 0; - height: calc(100% - 60px); + flex: 1; + min-height: 0; + padding: 8px 0; + overflow: hidden; .industry-item { display: flex; @@ -33,405 +62,197 @@ align-items: center; padding: 12px 20px; cursor: pointer; - transition: all 0.3s; + transition: all 0.2s; border-left: 3px solid transparent; + color: @jp-text-secondary; &:hover, &.active { - background-color: #f0f8ff; - border-left-color: #1890ff; - color: #1890ff; + background-color: @jp-primary-light; + border-left-color: @jp-primary; + color: @jp-primary; } .anticon { - color: #999; + color: @jp-text-muted; font-size: 12px; } &:hover .anticon, &.active .anticon { - color: #1890ff; + color: @jp-primary; } } } .pagination { + flex-shrink: 0; display: flex; justify-content: center; align-items: center; - padding: 16px; - border-top: 1px solid #f0f0f0; - height: 60px; + padding: 12px 16px; + border-top: 1px solid @jp-border; .ant-btn { border: none; box-shadow: none; + color: @jp-primary; } span { margin: 0 16px; - color: #666; + color: @jp-text-muted; + font-size: 13px; } } + + .industry-count { + flex-shrink: 0; + text-align: center; + font-size: 12px; + color: @jp-text-muted; + padding: 0 16px 12px; + } } - // 职业展示面板 .profession-panel { - height: 600px; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - overflow-y: auto; // 允许纵向滚动 - overflow-x: hidden; // 防止横向滚动 - - // 自定义滚动条样式 - &::-webkit-scrollbar { - width: 6px; - } - - &::-webkit-scrollbar-track { - background: #f1f1f1; - border-radius: 3px; - } - - &::-webkit-scrollbar-thumb { - background: #c1c1c1; - border-radius: 3px; - - &:hover { - background: #a8a8a8; - } - } - - .profession-title { - margin-bottom: 20px; - color: #1890ff; - text-align: center; - } + .jp-card-base(); + min-height: 420px; + height: 100%; .profession-content { - height: auto; // 让内容自然撑开 - overflow-y: visible; // 不再单独滚动 - overflow-x: hidden; // 水平不滚动 - padding-right: 0; // 滚动条由父级处理 - width: 100%; // 确保宽度 - box-sizing: border-box; + max-height: 340px; + overflow-y: auto; + padding-right: 4px; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: #d1d5db; + border-radius: 3px; + } .profession-category { - margin-bottom: 16px; // 减少间距 + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } .category-title { - margin-bottom: 8px; // 减少标题间距 - color: #333; + margin-bottom: 10px; + color: @jp-text-primary; font-weight: 600; - font-size: 14px; // 稍微减小字体 + font-size: 14px; } .profession-tags { display: flex; - flex-wrap: wrap; // 允许换行,实现纵向布局 - gap: 6px; // 标签间距 - width: 100%; // 确保容器占满宽度 - box-sizing: border-box; - overflow-x: hidden; // 不再水平滚动 - overflow-y: visible; // 不再单独滚动 - padding-bottom: 0; // 滚动条由父级处理 - - // 移除自定义滚动条样式,由父级处理 - &::-webkit-scrollbar { - display: none; // 隐藏子元素的滚动条 - } + flex-wrap: wrap; + gap: 8px; .profession-tag { - background: #f6f8fa; - border: 1px solid #e1e4e8; - color: #24292e; - border-radius: 4px; - padding: 3px 6px; + .jp-job-tag(); + padding: 4px 12px; cursor: pointer; - transition: all 0.3s; - font-size: 11px; - white-space: nowrap; // 标签本身不换行 - flex: 0 0 auto; // 不允许伸缩 - max-width: calc(33.333% - 4px); // 限制为三分之一宽度,留出间距 - min-width: 0; // 允许收缩到0 - overflow: hidden; // 超出部分隐藏 - text-overflow: ellipsis; // 超出时显示省略号 - box-sizing: border-box; - word-break: break-all; // 允许在任意字符间换行 - position: relative; - z-index: 1; + font-size: 13px; + transition: all 0.2s; &:hover { - max-width: none; // 悬停时显示完整内容 - white-space: normal; // 允许换行 - overflow: visible; - text-overflow: clip; - word-break: break-word; - background: #1890ff; - color: white; - border-color: #1890ff; - transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - z-index: 10; + background: @jp-primary !important; + color: #fff !important; + border-color: @jp-primary !important; } } } } - } - } - // 右侧内容区域 - .right-content-wrapper { - position: relative; - - .content-row { - position: relative; - } - - .ad-col { - transition: all 0.3s ease; - } - - // 当有浮动层时,广告区域右移 - &.has-profession { - .ad-col { - position: relative; - - // 添加左侧padding避免内容重叠 - padding-left: calc(50% + 8px); + .profession-empty { + display: flex; + align-items: center; + justify-content: center; + min-height: 200px; } } } - // 职业展示区域 - 浮动层 - .profession-panel-float { - position: absolute; - left: 0; - top: 0; - width: calc(50% - 8px); - z-index: 10; - animation: slideIn 0.3s ease-out; - pointer-events: auto; - - .close-btn { - position: absolute; - top: -10px; - right: -10px; - z-index: 11; - background: #fff; - border-radius: 50%; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - width: 32px; - height: 32px; + .jobs-section { + .jobs-section-header { display: flex; + justify-content: space-between; align-items: center; - justify-content: center; + margin-bottom: 20px; - &:hover { - background: #ff4d4f; - color: #fff; - transform: rotate(90deg); - transition: all 0.3s; - } - } - - .profession-panel { - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); - } - } - - @keyframes slideIn { - from { - opacity: 0; - transform: translateX(-20px); - } - to { - opacity: 1; - transform: translateX(0); - } - } - - // 广告区域 - .advertisement-area { - height: 600px; - overflow-y: auto; - transition: all 0.3s ease; - - .ad-card { - margin-bottom: 16px; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - transition: all 0.3s; - - &:hover { - transform: translateY(-2px); - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + .jobs-title { + .jp-section-title(); + margin: 0; } - // 横幅广告 - &.banner { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - height: 120px; + .view-more { + color: @jp-primary; + font-size: 14px; + cursor: pointer; - .banner-ad { - display: flex; - justify-content: space-between; - align-items: center; - height: 100%; - - .banner-content { - flex: 1; - - .banner-title { - color: white; - margin-bottom: 8px; - font-size: 18px; - } - - .banner-subtitle { - color: rgba(255, 255, 255, 0.8); - font-size: 12px; - } - } - - .banner-logo { - background: rgba(255, 255, 255, 0.2); - padding: 8px 12px; - border-radius: 4px; - font-size: 12px; - font-weight: bold; - } + &:hover { + color: @jp-primary-dark; } } - - // 卡片广告 - &.card { - height: 100px; - position: relative; - - &.purple { - background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); - } - - &.blue { - background: linear-gradient(135deg, #d299c2 0%, #fef9d7 100%); - } - - &.orange { - background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); - } - - .card-ad { - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - position: relative; - - .card-title { - margin-bottom: 4px; - font-size: 16px; - color: #333; - } - - .card-subtitle { - color: #666; - font-size: 12px; - } - - .card-description { - color: #999; - font-size: 10px; - margin-top: 4px; - } - - .ad-label { - position: absolute; - bottom: 8px; - right: 8px; - background: rgba(0, 0, 0, 0.1); - color: #666; - padding: 2px 6px; - border-radius: 4px; - font-size: 10px; - } - } - } - } - } - } - - // 职位列表区域 - .jobs-section { - background-color: #f5f5f5; - padding: 40px 20px; - - .jobs-container { - max-width: 1200px; - margin: 0 auto; - - .jobs-title { - text-align: center; - margin-bottom: 30px; - color: #1890ff; - font-weight: 600; } .job-card { height: 100%; - border-radius: 8px; - transition: all 0.3s; - background: white; + .jp-card-base(); cursor: pointer; + transition: all 0.2s; &:hover { - transform: translateY(-4px); - box-shadow: 0 6px 16px rgba(24, 144, 255, 0.2); + border-color: @jp-primary-border; + box-shadow: @jp-shadow-hover; } .job-header { display: flex; justify-content: space-between; - align-items: center; - margin-bottom: 12px; + align-items: flex-start; + margin-bottom: 10px; + gap: 8px; .job-title { margin: 0; font-size: 16px; - color: #333; + color: @jp-text-primary; font-weight: 600; flex: 1; } .job-salary { - color: #ff4d4f; - font-size: 16px; - font-weight: 600; + .jp-salary-text(); + font-size: 15px; white-space: nowrap; - margin-left: 12px; flex-shrink: 0; } } .job-company { display: block; - color: #666; - font-size: 14px; - margin-bottom: 12px; + color: @jp-text-muted; + font-size: 13px; + margin-bottom: 10px; } .job-info { display: flex; - gap: 12px; - margin-bottom: 12px; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 10px; .job-location, .job-experience, .job-education { - color: #999; + color: @jp-text-muted; font-size: 12px; - white-space: nowrap; } } @@ -441,13 +262,9 @@ gap: 6px; .job-tag { - background: #f6f8fa; - border: 1px solid #e1e4e8; - color: #666; - border-radius: 4px; - padding: 2px 8px; + .jp-job-tag(); font-size: 12px; - margin: 0; + padding: 2px 8px; } } } @@ -455,23 +272,15 @@ } } -// 响应式设计 -@media (max-width: 1200px) { - .job-portal { - .main-content { - .ant-col { - margin-bottom: 16px; - } - +@media (max-width: 992px) { + .job-portal .main-content { + .category-layout { .profession-panel { + margin-top: 16px; + min-height: auto; + .profession-content { - .profession-tags { - .profession-tag { - max-width: calc(50% - 4px); // 中等屏幕下占一半宽度 - font-size: 10px; // 进一步减小字体 - padding: 2px 4px; - } - } + max-height: none; } } } @@ -479,46 +288,8 @@ } @media (max-width: 768px) { - .job-portal { - .main-content { - .ant-col { - margin-bottom: 16px; - } - - .industry-panel, - .profession-panel, - .advertisement-area { - height: auto; - min-height: 300px; - } - - .profession-panel { - .profession-content { - .profession-category { - margin-bottom: 16px; - } - - .profession-tags { - gap: 4px; - - .profession-tag { - font-size: 10px; // 小屏幕下减小字体 - padding: 3px 5px; - } - } - } - } - } - - .jobs-section { - padding: 20px 10px; - - .jobs-container { - .jobs-title { - font-size: 20px; - margin-bottom: 20px; - } - } - } + .job-portal .main-content { + padding: 0 12px 32px; + margin-top: 12px; } } diff --git a/src/pages/JobPortal/index.tsx b/src/pages/JobPortal/index.tsx index 076bbb6..d43d6a9 100644 --- a/src/pages/JobPortal/index.tsx +++ b/src/pages/JobPortal/index.tsx @@ -4,7 +4,6 @@ import { Row, Col, Tag, - Space, Typography, Button, message @@ -12,7 +11,6 @@ import { import { RightOutlined, LeftOutlined, - CloseOutlined, EnvironmentOutlined } from '@ant-design/icons'; import { history } from '@umijs/max'; @@ -22,12 +20,6 @@ import './index.less'; const { Title, Text } = Typography; -// 行业分类数据现在从API获取 - -// 热门职位 -const hotJobs = ['Java', '产品经理', '前端开发工程师', '测试工程师', '运维工程师', '数据分析师', '平面设计']; - -// 学历字典映射 const educationMap: { [key: string]: string } = { '1': '大专', '2': '本科', @@ -36,7 +28,6 @@ const educationMap: { [key: string]: string } = { '0': '不限' }; -// 工作经验字典映射 const experienceMap: { [key: string]: string } = { '1': '1年以下', '2': '1-3年', @@ -46,58 +37,6 @@ const experienceMap: { [key: string]: string } = { '0': '不限' }; -// 广告数据 -const advertisements = [ - { - id: 1, - title: '成都高新区天府人才行动暨天府软件园 2025年秋季招聘校园行', - subtitle: '主办单位:国家数字服务出口基地(成都) 成都天府软件园有限公司', - type: 'banner', - logo: '新兴向蒙' - }, - { - id: 2, - title: '直聘简历', - subtitle: '写好简历 找好工作', - description: '免费设计 | 海量模板 智能润色', - type: 'card', - theme: 'purple' - }, - { - id: 3, - title: '最前端有"钱"途', - subtitle: 'Web前端工程师岗位热推', - type: 'card', - theme: 'blue', - isAd: true - }, - { - id: 4, - title: '算法解决一切', - subtitle: '算法工程师职位精选', - type: 'card', - theme: 'purple', - isAd: true - }, - { - id: 5, - title: '挑战最高年薪', - subtitle: 'Java工程师职位精选', - type: 'card', - theme: 'blue', - isAd: true - }, - { - id: 6, - title: '创造新次元', - subtitle: '高薪设计职位精选', - type: 'card', - theme: 'orange', - isAd: true - } -]; - -// 默认职位数据 const defaultJobs = [ { id: 1, @@ -178,76 +117,29 @@ const defaultJobs = [ experience: '3-7年', education: '硕士', tags: ['五险一金', '股票期权', '技术交流'] - }, - { - id: 9, - title: '运维工程师', - company: '青岛云计算科技公司', - salary: '12k-20k', - location: '青岛·李沧区', - experience: '2-4年', - education: '本科', - tags: ['五险一金', '带薪年假', '晋升空间'] - }, - { - id: 10, - title: 'Android开发工程师', - company: '青岛移动科技公司', - salary: '13k-22k', - location: '青岛·市南区', - experience: '3-5年', - education: '本科', - tags: ['五险一金', '年终奖', '弹性工作'] - }, - { - id: 11, - title: 'iOS开发工程师', - company: '青岛移动科技公司', - salary: '13k-22k', - location: '青岛·市南区', - experience: '3-5年', - education: '本科', - tags: ['五险一金', '年终奖', '弹性工作'] - }, - { - id: 12, - title: '全栈开发工程师', - company: '青岛创新科技公司', - salary: '15k-28k', - location: '青岛·崂山区', - experience: '3-6年', - education: '本科', - tags: ['五险一金', '双休', '技术成长'] } ]; const JobPortalPage: React.FC = () => { - const [selectedIndustry, setSelectedIndustry] = useState(''); const [hoveredIndustry, setHoveredIndustry] = useState(''); const [currentPage, setCurrentPage] = useState(1); - const [showProfession, setShowProfession] = useState(false); const [jobTitleData, setJobTitleData] = useState(null); const [jobRecommendData, setJobRecommendData] = useState(null); const [loading, setLoading] = useState(false); - const [itemsPerPage] = useState(10); // 每页显示的行业数量 + const [itemsPerPage] = useState(6); const [totalPages, setTotalPages] = useState(1); - // 调用职位接口获取数据 useEffect(() => { const fetchJobTitleData = async () => { try { setLoading(true); const response = await getJobTitleTreeSelect(); setJobTitleData(response); - // 计算总页数 if (response?.data && response.data.length > 0) { - const totalPages = Math.ceil(response.data.length / itemsPerPage); - setTotalPages(totalPages); - // 设置默认选中的第一个行业 - setSelectedIndustry(response.data[0].id.toString()); + const pages = Math.ceil(response.data.length / itemsPerPage); + setTotalPages(pages); setHoveredIndustry(response.data[0].id.toString()); } - // message.success('职位数据获取成功'); } catch (error) { message.error('获取职位数据失败'); } finally { @@ -259,8 +151,8 @@ const JobPortalPage: React.FC = () => { try { const response = await getJobRecommend(); setJobRecommendData(response); - } catch (error) { - // 静默处理错误,不影响用户体验 + } catch { + // 静默处理 } }; @@ -270,47 +162,24 @@ const JobPortalPage: React.FC = () => { const handleIndustryHover = (industryId: string) => { setHoveredIndustry(industryId); - setShowProfession(true); - }; - - const handleIndustryClick = (industryId: string) => { - setSelectedIndustry(industryId); - }; - - const handleIndustryMouseLeave = () => { - // 用户离开左侧区域时,如果没有鼠标在右侧,则隐藏 - // 这里交给onMouseLeave处理 - }; - - const handleRightContentMouseLeave = () => { - setShowProfession(false); - }; - - const handleProfessionPanelEnter = () => { - // 鼠标进入浮动层时保持显示 - 阻止关闭 - }; - - const handleProfessionPanelLeave = () => { - setShowProfession(false); }; const getCurrentProfessions = () => { if (!jobTitleData?.data) return []; - const industry = jobTitleData.data.find((item: any) => item.id.toString() === hoveredIndustry); return industry?.children || []; }; - // 获取当前页的行业数据 const getCurrentPageIndustries = () => { if (!jobTitleData?.data) return []; - const startIndex = (currentPage - 1) * itemsPerPage; - const endIndex = startIndex + itemsPerPage; - return jobTitleData.data.slice(startIndex, endIndex); + return jobTitleData.data.slice(startIndex, startIndex + itemsPerPage); + }; + + const getHoveredIndustryLabel = () => { + return jobTitleData?.data?.find((item: any) => item.id.toString() === hoveredIndustry)?.label || ''; }; - // 格式化薪资显示 const formatSalary = (minSalary: number, maxSalary: number) => { if (minSalary && maxSalary) { return `${(minSalary / 1000).toFixed(0)}k-${(maxSalary / 1000).toFixed(0)}k`; @@ -323,45 +192,99 @@ const JobPortalPage: React.FC = () => { }; const handlePageChange = (direction: 'prev' | 'next') => { + let nextPage = currentPage; if (direction === 'prev' && currentPage > 1) { - setCurrentPage(currentPage - 1); - // 切换页面时隐藏职业面板 - setShowProfession(false); + nextPage = currentPage - 1; } else if (direction === 'next' && currentPage < totalPages) { - setCurrentPage(currentPage + 1); - // 切换页面时隐藏职业面板 - setShowProfession(false); + nextPage = currentPage + 1; + } else { + return; + } + setCurrentPage(nextPage); + if (jobTitleData?.data) { + const firstOnPage = jobTitleData.data[(nextPage - 1) * itemsPerPage]; + if (firstOnPage) { + setHoveredIndustry(firstOnPage.id.toString()); + } } }; const handleJobClick = (job: any) => { - // 点击岗位卡片时跳转到岗位详情,传递完整的职位数据 history.push(`/job-portal/detail`, { jobData: job }); }; - const handleProfessionTagClick = (jobId: number, name: any) => { - // alert(name); - // 点击职位标签时跳转到职位列表 - history.push(`/job-portal/list`, { queryParams: { name: name } }); + const handleProfessionTagClick = (name: string) => { + history.push(`/job-portal/list`, { queryParams: { name } }); }; + const renderJobCards = () => { + const jobs = jobRecommendData?.data; + if (jobs?.length) { + return jobs.map((job: any) => ( + + handleJobClick(job)} + > +
+ {job.jobTitle} + {formatSalary(job.minSalary, job.maxSalary)} +
+ {job.companyName} +
+  {job.jobLocation} + {experienceMap[job.experience] || '不限'} + {educationMap[job.education] || '不限'} +
+
+ {job.jobCategory && {job.jobCategory}} + {job.vacancies && 招聘{job.vacancies}人} +
+
+ + )); + } + return defaultJobs.map(job => ( + + handleJobClick(job)} + > +
+ {job.title} + {job.salary} +
+ {job.company} +
+ {job.location} + {job.experience} + {job.education} +
+
+ {job.tags.map((tag, index) => ( + {tag} + ))} +
+
+ + )); + }; return (
- + - {/* 主要内容区域 */}
- - {/* 左侧行业分类 */} - - setShowProfession(true)} - onMouseLeave={handleIndustryMouseLeave} - loading={loading} - > + + + +
+ 行业分类 +
{getCurrentPageIndustries().map((industry: any) => (
{ hoveredIndustry === industry.id.toString() ? 'active' : '' }`} onMouseEnter={() => handleIndustryHover(industry.id.toString())} - onClick={() => handleIndustryClick(industry.id.toString())} > {industry.label} @@ -393,152 +315,55 @@ const JobPortalPage: React.FC = () => { />
{jobTitleData?.data && ( -
- 共 {jobTitleData.data.length} 个行业 -
+
共 {jobTitleData.data.length} 个行业
)} - {/* 右侧内容区域 */} - - - {/* 广告区域 */} - -
- {advertisements.map(ad => ( - - {ad.type === 'banner' ? ( -
-
- {ad.title} - {ad.subtitle} -
-
{ad.logo}
-
- ) : ( -
- {ad.title} - {ad.subtitle} - {ad.description && ( - {ad.description} - )} - {ad.isAd &&
广告
} -
- )} -
- ))} -
- -
- - {/* 职业展示区域 - 浮动层 */} - {showProfession && ( -
-
- - {/* 职位列表区域 */} -
-
- 热门职位推荐 - - {jobRecommendData?.data?.map((job: any) => ( - - handleJobClick(job)} - > -
- {job.jobTitle} - {formatSalary(job.minSalary, job.maxSalary)} -
- {job.companyName} -
-  {job.jobLocation} - {experienceMap[job.experience] || '不限'} - {educationMap[job.education] || '不限'} -
-
- {job.jobCategory && {job.jobCategory}} - {/* {job.dataSource && {job.dataSource}} */} - {job.vacancies && 招聘{job.vacancies}人} -
-
- - )) || defaultJobs.map(job => ( - - handleJobClick(job)} - > -
- {job.title} - {job.salary} -
- {job.company} -
- {job.location} - {job.experience} - {job.education} -
-
- {job.tags.map((tag, index) => ( - {tag} - ))} -
-
- - ))} -
+
+
+ 热门职位推荐 + history.push('/job-portal/list')}> + 查看更多 > + +
+ {renderJobCards()}
diff --git a/src/pages/JobPortal/theme.less b/src/pages/JobPortal/theme.less new file mode 100644 index 0000000..d6cb4fd --- /dev/null +++ b/src/pages/JobPortal/theme.less @@ -0,0 +1,56 @@ +// 石城智慧就业 - 门户 UI 设计变量 +@jp-primary: #1890ff; +@jp-primary-dark: #096dd9; +@jp-primary-light: #e6f4ff; +@jp-primary-border: #91caff; +@jp-bg-page: #f0f4f8; +@jp-bg-card: #ffffff; +@jp-border: #e8ecf0; +@jp-text-primary: #1a1a1a; +@jp-text-secondary: #595959; +@jp-text-muted: #8c8c8c; +@jp-radius: 8px; +@jp-radius-lg: 12px; +@jp-shadow-card: 0 2px 8px rgba(0, 0, 0, 0.06); +@jp-shadow-hover: 0 4px 16px rgba(24, 144, 255, 0.12); +@jp-content-width: 1200px; +@jp-nav-bg: #1890ff; +@jp-nav-height: 48px; + +.jp-page-container() { + max-width: @jp-content-width; + margin: 0 auto; + padding: 0 20px; +} + +.jp-card-base() { + background: @jp-bg-card; + border-radius: @jp-radius; + border: 1px solid @jp-border; + box-shadow: @jp-shadow-card; +} + +.jp-section-title() { + font-size: 22px; + font-weight: 600; + color: @jp-text-primary; +} + +.jp-salary-text() { + color: @jp-primary; + font-weight: 600; +} + +.jp-job-tag() { + background: @jp-primary-light !important; + border: 1px solid @jp-primary-border !important; + color: @jp-primary !important; + border-radius: 4px; + margin: 0; +} + +// 门户页面根容器 +.job-portal-page-root { + min-height: 100vh; + background-color: @jp-bg-page; +} diff --git a/src/pages/ThirdPartyRedirect.tsx b/src/pages/ThirdPartyRedirect.tsx index 5c50d2e..cb6bc3e 100644 --- a/src/pages/ThirdPartyRedirect.tsx +++ b/src/pages/ThirdPartyRedirect.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { Spin, message } from 'antd'; import { history, useLocation, useModel } from '@umijs/max'; -import { getAccessToken } from '@/access'; +import { clearAllClientStorage, getAccessToken } from '@/access'; import { getRoutersInfo, setRemoteMenu } from '@/services/session'; import { flushSync } from 'react-dom'; @@ -103,6 +103,9 @@ export default function ThirdPartyRedirect() { if (pageName === 'personal-center') { history.replace(`/job-portal/${pageName}`); } + if (pageName === 'policy') { + history.replace(`/job-portal/${pageName}`); + } return; } else { const menus = await getRoutersInfo(); @@ -124,6 +127,30 @@ export default function ThirdPartyRedirect() { } }; + /** URL 未带 token 时清除本地登录态(与退出登录一致) */ + const performLogout = () => { + clearAllClientStorage(); + setRemoteMenu(null); + flushSync(() => { + setInitialState((s) => ({ + ...s, + currentUser: undefined, + })); + }); + }; + + const redirectByPageName = (params: URLSearchParams, pageName: string | null) => { + if (!pageName) { + history.replace('/job-portal'); + return; + } + if (pageName === 'jobDetail') { + history.replace(`/job-portal/detail/${params.get('jobId')}`); + return; + } + history.replace(`/job-portal/${pageName}`); + }; + const handleLoginRedirect = async () => { try { setIsError(false); @@ -132,11 +159,8 @@ export default function ThirdPartyRedirect() { const token = params.get('token'); const pageName = params.get('pageName'); - if (!code && !token && !pageName) { - setIsError(true); - setStatus(STATUS_TEXT.noCredential); - } - if (code || token) { + // token 与 code 互斥,仅其一用于 SSO 换票 + if (token || code) { const sessionToken = getAccessToken(); if (!sessionToken) { setIsError(true); @@ -147,21 +171,10 @@ export default function ThirdPartyRedirect() { await fetchUserInfo(); return; } - if (!token) { - if (!pageName) { - history.replace(`/job-portal`); - } - if (pageName === 'message') { - history.replace(`/job-portal/${pageName}`); - } - if (pageName === 'jobDetail') { - history.replace(`/job-portal/detail/${params.get('jobId')}`); - } - if (pageName === 'personal-center') { - history.replace(`/job-portal/${pageName}`); - } - return; - } + + // URL 无 token/code:退出登录后跳转(无 pageName 则去求职门户首页) + performLogout(); + redirectByPageName(params, pageName); } catch (error) { console.error('单点进入失败:', error); setIsError(true); diff --git a/src/services/cms/policyInfo.ts b/src/services/cms/policyInfo.ts index bad090a..64a7306 100644 --- a/src/services/cms/policyInfo.ts +++ b/src/services/cms/policyInfo.ts @@ -8,10 +8,11 @@ export async function getPolicyInfoList(params?: API.PolicyInfo.Params) { }); } -// 获取政策详情 -export async function getPolicyInfoDetail(id: number) { - return request(`/api/cms/policyInfo/${id}`, { +/** 获取政策详情 GET /cms/policyInfo/detail?id= */ +export async function getPolicyInfoDetail(id: number | string) { + return request('/api/cms/policyInfo/detail', { method: 'GET', + params: { id }, }); } diff --git a/src/services/jobportal/competitiveness.ts b/src/services/jobportal/competitiveness.ts index 3c989ee..107799a 100644 --- a/src/services/jobportal/competitiveness.ts +++ b/src/services/jobportal/competitiveness.ts @@ -1,4 +1,5 @@ import { request } from '@umijs/max'; +import { isJobPortalLoggedIn } from '@/utils/jobPortalAuth'; export interface JobCompetitivenessRadar { skill?: number; @@ -17,9 +18,22 @@ export interface GetJobCompetitivenessResponse { radarChart?: JobCompetitivenessRadar; } +export const getEmptyCompetitivenessRadar = () => [ + { item: '技能', score: 0 }, + { item: '工作经验', score: 0 }, + { item: '学历', score: 0 }, + { item: '薪资', score: 0 }, + { item: '年龄', score: 0 }, + { item: '工作地', score: 0 }, +]; + +/** 职位竞争力 GET /app/job/competitiveness/{jobId}(需登录) */ export async function getJobCompetitiveness(jobId: number) { + if (!isJobPortalLoggedIn()) { + return null; + } return request(`/app/job/competitiveness/${jobId}`, { - method: 'GET' + method: 'GET', }); } diff --git a/src/types/cms/policyInfo.d.ts b/src/types/cms/policyInfo.d.ts index 762e7d1..9d5ebea 100644 --- a/src/types/cms/policyInfo.d.ts +++ b/src/types/cms/policyInfo.d.ts @@ -29,7 +29,7 @@ declare namespace API.PolicyInfo { zclx: string; zcLevel: string; sourceUnit: string; - acceptUnit: string; + acceptUnit?: string | null; publishTime: string; zcContent?: string; subsidyStandard?: string; diff --git a/src/utils/jobPortalAuth.ts b/src/utils/jobPortalAuth.ts new file mode 100644 index 0000000..3c0b61f --- /dev/null +++ b/src/utils/jobPortalAuth.ts @@ -0,0 +1,38 @@ +import { Modal } from 'antd'; +import { getAccessToken } from '@/access'; + +/** 人社门户登录页 */ +export const PORTAL_LOGIN_URL = 'http://218.31.252.15:9081/hrss-web-vue/login'; + +export const isJobPortalLoggedIn = (): boolean => { + if (getAccessToken()) { + return true; + } + try { + const cached = localStorage.getItem('userInfo'); + if (cached) { + const userInfo = JSON.parse(cached); + return !!userInfo?.userId; + } + } catch { + // ignore + } + return false; +}; + +/** 未登录时弹窗提示并跳转登录页,已登录返回 true */ +export const ensureJobPortalLogin = (actionHint = '该操作'): boolean => { + if (isJobPortalLoggedIn()) { + return true; + } + Modal.confirm({ + title: '请先登录', + content: `登录后才可${actionHint},是否前往登录?`, + okText: '去登录', + cancelText: '取消', + onOk: () => { + window.location.href = PORTAL_LOGIN_URL; + }, + }); + return false; +}; diff --git a/src/utils/jobPortalDict.ts b/src/utils/jobPortalDict.ts new file mode 100644 index 0000000..3df68d2 --- /dev/null +++ b/src/utils/jobPortalDict.ts @@ -0,0 +1,64 @@ +import type { DictValueEnumObj } from '@/components/DictTag'; + +/** 字典值转展示文案 */ +export function getDictLabel( + enums: DictValueEnumObj | Record, + value?: string | number | null, +): string { + if (value === null || value === undefined || value === '') { + return ''; + } + const strVal = String(value); + if (!enums || !Object.keys(enums).length) { + return strVal; + } + + const direct = enums[strVal] ?? enums[Number(value)]; + if (direct) { + return direct.label || direct.text || strVal; + } + + const matched = Object.values(enums).find((item: any) => { + return ( + String(item.value) === strVal || + String(item.key) === strVal || + String(item.id) === strVal + ); + }); + if (matched) { + return (matched as any).label || (matched as any).text || strVal; + } + + return strVal; +} + +/** 树形数据 id 转 label(岗位分类 / 行业等) */ +export function findTreeLabelById( + nodes: any[] | undefined, + id?: string | number | null, +): string { + if (id === null || id === undefined || id === '') { + return ''; + } + const idStr = String(id); + if (!nodes?.length) { + return ''; + } + + for (const node of nodes) { + const nodeId = String(node.id ?? node.value ?? ''); + if (nodeId === idStr) { + return node.label ?? node.title ?? ''; + } + if (node.children?.length) { + const childLabel = findTreeLabelById(node.children, id); + if (childLabel) { + return childLabel; + } + } + } + return ''; +} + +/** @deprecated 使用 findTreeLabelById */ +export const findIndustryLabelInTree = findTreeLabelById;