feat: 优化岗位系统功能和界面
- 添加已投递岗位展示功能,与企业岗位列表集成 - 修复简历版本选择独立状态管理bug - 统一岗位卡片和详情页面的标签样式 - 为未投递岗位添加剩余数量显示和警告图标 - 优化雷达图和仪表盘的显示效果 - 调整岗位详情弹窗的背景和宽度 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
.job-info-modal-content {
|
||||
max-height: 80vh;
|
||||
width: 844px;
|
||||
width: 720px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -8,8 +8,8 @@
|
||||
justify-content: flex-start;
|
||||
background-color: #f2f3f5;
|
||||
background-image: url("@/assets/images/CompanyJobsPage/background.png");
|
||||
background-size: auto;
|
||||
background-position: top right;
|
||||
background-size: 100% auto;
|
||||
background-position: top center;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
@@ -194,6 +194,45 @@
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
/* 根据岗位相关标签内容设置不同颜色 */
|
||||
.job-category-tag[data-category="专业相关岗位"] {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.job-category-tag[data-category="非专业相关岗位"] {
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%);
|
||||
}
|
||||
|
||||
.job-category-tag[data-category="人才出海岗位"] {
|
||||
background: linear-gradient(135deg, #00d2ff 0%, #3a7bd5 100%);
|
||||
}
|
||||
|
||||
.job-remaining-positions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
color: #ff4d4f;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
|
||||
.warning-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background-color: #ff4d4f;
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
margin-right: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.job-info-modal-content-position-info-num {
|
||||
font-size: 14px;
|
||||
@@ -221,14 +260,14 @@
|
||||
margin-top: 10px;
|
||||
|
||||
.job-info-modal-info-tag {
|
||||
background-color: #e5e6eb;
|
||||
background-color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 5px;
|
||||
padding: 1px 8px;
|
||||
color: #1d2129;
|
||||
padding: 4px 12px;
|
||||
color: #86909c;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border-radius: 2px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,12 @@ export default ({ visible, onClose, data, directToResume = false, hideDeliverBut
|
||||
const [resumeModalShow, setResumeModalShow] = useState(directToResume);
|
||||
const [resumeInfoModalShow, setResumeInfoModalShow] = useState(false);
|
||||
const [resumeInfoData, setResumeInfoData] = useState(null);
|
||||
const [currentResumeId, setCurrentResumeId] = useState(null); // 当前查看的简历ID
|
||||
const [resumeList, setResumeList] = useState([]); // 简历列表
|
||||
const [listPage, setListPage] = useState(1);
|
||||
const [listHasMore, setListHasMore] = useState(true);
|
||||
const [permissionModalVisible, setPermissionModalVisible] = useState(false);
|
||||
const [selectedVersion, setSelectedVersion] = useState("2"); // 默认选择个人修改版
|
||||
const [selectedVersions, setSelectedVersions] = useState({}); // 每个简历的版本选择,使用简历ID作为key
|
||||
|
||||
// 处理directToResume参数变化
|
||||
useEffect(() => {
|
||||
@@ -84,12 +85,12 @@ export default ({ visible, onClose, data, directToResume = false, hideDeliverBut
|
||||
resumeTitle: item.title,
|
||||
jobPosition: data?.position,
|
||||
company: data?.company,
|
||||
resumeVersion: selectedVersion // 添加版本信息
|
||||
resumeVersion: selectedVersions[item.id] || "2" // 添加版本信息
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
// 投递成功,显示成功提示
|
||||
const versionText = selectedVersion === "1" ? "原始版" : "个人修改版";
|
||||
const versionText = (selectedVersions[item.id] || "2") === "1" ? "原始版" : "个人修改版";
|
||||
toast.success(`简历"${item.title}"(${versionText})投递成功!`);
|
||||
|
||||
// 关闭模态框
|
||||
@@ -148,13 +149,14 @@ export default ({ visible, onClose, data, directToResume = false, hideDeliverBut
|
||||
resumeTitle: item.title,
|
||||
position: item.position,
|
||||
industry: item.industry,
|
||||
selectedVersion: selectedVersion,
|
||||
selectedVersion: selectedVersions[item.id] || "2",
|
||||
hasContent: !!positionTemplate?.content,
|
||||
hasOriginal: !!positionTemplate?.content?.original,
|
||||
hasModified: !!positionTemplate?.content?.modified
|
||||
});
|
||||
|
||||
setResumeInfoData(resumeData);
|
||||
setCurrentResumeId(item.id); // 记录当前简历ID
|
||||
setResumeInfoModalShow(true);
|
||||
} else {
|
||||
toast.error('加载简历数据失败');
|
||||
@@ -198,9 +200,14 @@ export default ({ visible, onClose, data, directToResume = false, hideDeliverBut
|
||||
<div className="version-selector">
|
||||
<Select
|
||||
placeholder="选择版本"
|
||||
value={selectedVersion}
|
||||
value={selectedVersions[item.id] || "2"}
|
||||
style={{ width: 120, fontSize: '12px' }}
|
||||
onChange={(value) => setSelectedVersion(value)}
|
||||
onChange={(value) => {
|
||||
setSelectedVersions(prev => ({
|
||||
...prev,
|
||||
[item.id]: value
|
||||
}));
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Select.Option value="1">原始版</Select.Option>
|
||||
@@ -231,13 +238,22 @@ export default ({ visible, onClose, data, directToResume = false, hideDeliverBut
|
||||
</span>
|
||||
{/* 岗位相关标签 */}
|
||||
{(data?.jobCategoryTag || data?.jobCategory) && (
|
||||
<span className="job-category-tag">
|
||||
<span
|
||||
className="job-category-tag"
|
||||
data-category={data?.jobCategoryTag || data?.jobCategory}
|
||||
>
|
||||
{data?.jobCategoryTag || data?.jobCategory}
|
||||
</span>
|
||||
)}
|
||||
<span className="job-info-modal-content-position-info-num">
|
||||
该岗位仅剩{data?.remainingPositions}人
|
||||
</span>
|
||||
|
||||
{/* 岗位剩余量 - 仅未投递岗位显示 */}
|
||||
{!data?.isDelivered && data?.remainingPositions && (
|
||||
<span className="job-remaining-positions">
|
||||
<i className="warning-icon">!</i>
|
||||
岗位招聘数量仅剩{data?.remainingPositions}名
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span className="job-info-modal-content-position-info-salary">
|
||||
{data?.salary}
|
||||
</span>
|
||||
@@ -314,10 +330,11 @@ export default ({ visible, onClose, data, directToResume = false, hideDeliverBut
|
||||
<ResumeInfoModal
|
||||
visible={resumeInfoModalShow}
|
||||
data={resumeInfoData}
|
||||
initialVersion={selectedVersion}
|
||||
initialVersion={selectedVersions[currentResumeId] || "2"}
|
||||
onClose={() => {
|
||||
setResumeInfoModalShow(false);
|
||||
setResumeInfoData(null);
|
||||
setCurrentResumeId(null);
|
||||
}}
|
||||
/>
|
||||
<PermissionModal
|
||||
|
||||
@@ -73,6 +73,38 @@
|
||||
color: #86909c;
|
||||
}
|
||||
}
|
||||
|
||||
&.delivered {
|
||||
background-color: #f7f8fa;
|
||||
background-image: none;
|
||||
border-color: #e5e6eb;
|
||||
opacity: 0.9;
|
||||
|
||||
&:hover {
|
||||
border-color: #c9cdd4;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
background-color: #f2f3f5;
|
||||
}
|
||||
|
||||
.company-jobs-info-position {
|
||||
color: #86909c;
|
||||
}
|
||||
|
||||
.company-jobs-info-tags {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.company-jobs-info-position-salary {
|
||||
color: #c9cdd4;
|
||||
}
|
||||
|
||||
.company-jobs-info-category-tag {
|
||||
opacity: 0.7;
|
||||
background-color: #f2f3f5;
|
||||
color: #86909c;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
@@ -146,14 +178,14 @@
|
||||
margin-top: 5px;
|
||||
|
||||
.company-jobs-info-tag {
|
||||
background-color: #fff;
|
||||
background-color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 5px;
|
||||
padding: 1px 8px;
|
||||
color: #4e5969;
|
||||
padding: 4px 12px;
|
||||
color: #86909c;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border-radius: 2px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
@@ -229,6 +261,25 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.delivered {
|
||||
background-color: #f0f5ff;
|
||||
color: #52c41a;
|
||||
border: 1px solid #b7eb8f;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f5ff;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
> i {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.company-jobs-info-deadline {
|
||||
|
||||
@@ -12,14 +12,47 @@ export default ({ className = "", data = [], backgroundColor }) => {
|
||||
|
||||
const handleJobClick = async (e, item) => {
|
||||
e.stopPropagation();
|
||||
const res = await getJobsDetail(item.id);
|
||||
if (res.success) {
|
||||
// Mock数据已经是前端格式,不需要映射
|
||||
setJobInfoData(res.data);
|
||||
setDirectToResume(false); // 点击岗位条目,显示详情
|
||||
|
||||
// 如果是已投递的岗位,直接使用岗位本身的数据
|
||||
if (item.isDelivered) {
|
||||
// 已投递岗位已经包含了所有必要的数据,需要转换为Modal期望的格式
|
||||
setJobInfoData({
|
||||
id: item.originalInterviewId || item.id,
|
||||
position: item.position,
|
||||
salary: item.salary,
|
||||
location: item.location,
|
||||
education: item.education,
|
||||
tags: item.tags,
|
||||
jobCategory: item.jobCategory,
|
||||
deadline: item.deadline,
|
||||
welfare: item.welfare,
|
||||
isDelivered: true,
|
||||
interviewTime: item.interviewTime,
|
||||
interviewStatus: item.interviewStatus,
|
||||
// 将详细信息放在details对象中,以匹配Modal的期望格式
|
||||
details: {
|
||||
description: item.description || "",
|
||||
requirements: item.requirements ?
|
||||
(typeof item.requirements === 'string' ?
|
||||
item.requirements.split(/\d+\.\s*/).filter(r => r.trim()) :
|
||||
item.requirements) : [],
|
||||
requirementsText: typeof item.requirements === 'string' ? item.requirements : "",
|
||||
companyInfo: item.companyInfo || ""
|
||||
}
|
||||
});
|
||||
setDirectToResume(false);
|
||||
setJobInfoModalVisible(true);
|
||||
} else {
|
||||
toast.error(res.message);
|
||||
// 未投递的岗位,从服务获取详情
|
||||
const res = await getJobsDetail(item.id);
|
||||
if (res.success) {
|
||||
// Mock数据已经是前端格式,不需要映射
|
||||
setJobInfoData(res.data);
|
||||
setDirectToResume(false); // 点击岗位条目,显示详情
|
||||
setJobInfoModalVisible(true);
|
||||
} else {
|
||||
toast.error(res.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,11 +150,11 @@ export default ({ className = "", data = [], backgroundColor }) => {
|
||||
return (
|
||||
<li
|
||||
key={item.id}
|
||||
className={`company-jobs-page-left-list-item ${(item.isExpired || item.status === 'expired') ? 'expired' : ''}`}
|
||||
className={`company-jobs-page-left-list-item ${(item.isExpired || item.status === 'expired') ? 'expired' : ''} ${item.isDelivered ? 'delivered' : ''}`}
|
||||
style={{ backgroundColor }}
|
||||
onClick={(e) => handleJobClick(e, item)}
|
||||
>
|
||||
<i className={`icon icon-${item?.jobType}`}></i>
|
||||
|
||||
<div className="company-jobs-info">
|
||||
<div className="company-jobs-info-position-wrapper">
|
||||
<p className="company-jobs-info-position">{item?.position}</p>
|
||||
@@ -144,21 +177,27 @@ export default ({ className = "", data = [], backgroundColor }) => {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="company-jobs-info-position-count">
|
||||
岗位招聘数量仅剩{item?.remainingPositions}名
|
||||
</p>
|
||||
{!item.isDelivered && !item.isExpired && item.status !== 'expired' && (
|
||||
<p className="company-jobs-info-position-count">
|
||||
岗位招聘数量仅剩{item?.remainingPositions}名
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="company-jobs-btn-wrapper">
|
||||
<p className="company-jobs-info-position-salary">
|
||||
{item?.salary}
|
||||
</p>
|
||||
<button
|
||||
className={`company-jobs-btn ${(item.isExpired || item.status === 'expired') ? 'disabled' : ''}`}
|
||||
onClick={(e) => handleDeliverClick(e, item)}
|
||||
disabled={item.isExpired || item.status === 'expired'}
|
||||
className={`company-jobs-btn ${(item.isExpired || item.status === 'expired') ? 'disabled' : item.isDelivered ? 'delivered' : ''}`}
|
||||
onClick={(e) => item.isDelivered ? e.stopPropagation() : handleDeliverClick(e, item)}
|
||||
disabled={item.isExpired || item.status === 'expired' || item.isDelivered}
|
||||
>
|
||||
<i />
|
||||
<span>{(item.isExpired || item.status === 'expired') ? '已过期' : '投递'}</span>
|
||||
<span>{
|
||||
(item.isExpired || item.status === 'expired') ? '已过期' :
|
||||
item.isDelivered ? '已投递' :
|
||||
'投递'
|
||||
}</span>
|
||||
</button>
|
||||
{item?.deadline && (
|
||||
<p className="company-jobs-info-deadline">
|
||||
@@ -175,6 +214,7 @@ export default ({ className = "", data = [], backgroundColor }) => {
|
||||
visible={jobInfoModalVisible}
|
||||
onClose={onClickJobInfoModalClose}
|
||||
directToResume={directToResume}
|
||||
hideDeliverButton={jobInfoData?.isDelivered || false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -45,25 +45,60 @@ const CompanyJobsPage = () => {
|
||||
});
|
||||
|
||||
if (res?.success) {
|
||||
// 设置岗位数据
|
||||
if (res.data?.jobs) {
|
||||
// 设置面试数据
|
||||
let interviewsData = [];
|
||||
if (res.data?.interviews) {
|
||||
// Mock数据已经是前端格式,直接使用不需要映射
|
||||
const jobs = res.data.jobs.list || [];
|
||||
setJobs(jobs);
|
||||
setJobsListHasMore(res.data.jobs.hasMore);
|
||||
if (jobs.length > 0) {
|
||||
setJobsListPage(2); // 下次从第2页开始
|
||||
interviewsData = res.data.interviews.list || [];
|
||||
setInterviews(interviewsData);
|
||||
setInterviewsHasMore(res.data.interviews.hasMore);
|
||||
if (interviewsData.length > 0) {
|
||||
setInterviewsPage(2); // 下次从第2页开始
|
||||
}
|
||||
}
|
||||
|
||||
// 设置面试数据
|
||||
if (res.data?.interviews) {
|
||||
// 设置岗位数据 - 包含已投递的岗位
|
||||
if (res.data?.jobs) {
|
||||
// Mock数据已经是前端格式,直接使用不需要映射
|
||||
const interviews = res.data.interviews.list || [];
|
||||
setInterviews(interviews);
|
||||
setInterviewsHasMore(res.data.interviews.hasMore);
|
||||
if (interviews.length > 0) {
|
||||
setInterviewsPage(2); // 下次从第2页开始
|
||||
const jobsList = res.data.jobs.list || [];
|
||||
|
||||
// 从面试数据中提取已投递的岗位信息
|
||||
const deliveredJobs = interviewsData.map(interview => {
|
||||
// 确保有完整的岗位数据
|
||||
const jobData = interview.job || {};
|
||||
return {
|
||||
id: `delivered-${interview.id}`, // 使用特殊的ID标识已投递岗位
|
||||
position: interview.position,
|
||||
isDelivered: true, // 标记为已投递
|
||||
interviewTime: interview.interviewTime,
|
||||
interviewStatus: interview.statusText,
|
||||
originalInterviewId: interview.id,
|
||||
// 从job对象中提取所有必要字段
|
||||
salary: jobData.salary || "面议",
|
||||
tags: jobData.tags || [],
|
||||
location: jobData.location || "待定",
|
||||
education: jobData.education || "待定",
|
||||
jobCategory: jobData.jobCategory || "专业相关岗位",
|
||||
remainingPositions: jobData.remainingPositions || 5,
|
||||
deadline: jobData.deadline || "2025-12-31",
|
||||
jobType: jobData.jobType || "job",
|
||||
requirements: jobData.requirements || "",
|
||||
description: jobData.description || "",
|
||||
welfare: jobData.welfare || [],
|
||||
companyInfo: jobData.companyInfo || ""
|
||||
};
|
||||
}).filter(job => job.position); // 过滤掉没有岗位信息的项
|
||||
|
||||
// 分离未投递和已过期的岗位
|
||||
const activeJobs = jobsList.filter(job => !job.isExpired && job.status !== 'expired');
|
||||
const expiredJobs = jobsList.filter(job => job.isExpired || job.status === 'expired');
|
||||
|
||||
// 按照顺序合并:未投递 -> 已投递 -> 已过期
|
||||
const allJobs = [...activeJobs, ...deliveredJobs, ...expiredJobs];
|
||||
setJobs(allJobs);
|
||||
setJobsListHasMore(res.data.jobs.hasMore);
|
||||
if (allJobs.length > 0) {
|
||||
setJobsListPage(2); // 下次从第2页开始
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user