feat: 优化岗位系统功能和界面

- 添加已投递岗位展示功能,与企业岗位列表集成
- 修复简历版本选择独立状态管理bug
- 统一岗位卡片和详情页面的标签样式
- 为未投递岗位添加剩余数量显示和警告图标
- 优化雷达图和仪表盘的显示效果
- 调整岗位详情弹窗的背景和宽度

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
KQL
2025-09-11 11:34:05 +08:00
parent 15db293d5b
commit 60bd9bb142
8 changed files with 518 additions and 118 deletions

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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}
/>
</>
);

View File

@@ -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页开始
}
}

View File

@@ -14,19 +14,57 @@ export default function RadarChart({
lineClolr = "#DCDFFF",
areaColor = "#CCCDFC",
areaBorderColor = "#BDB5FF",
isGreenTheme = false, // 新增参数:是否使用绿色主题
}) {
const chartRef = useRef(null);
const chartInstance = useRef(null);
// 获取实际数据值
const actualData = value || data;
// 创建渐变色配置
const gradientColor = isGreenTheme ? {
type: 'radial',
x: 0.5,
y: 0.5,
r: 0.5,
colorStops: [{
offset: 0,
color: 'rgba(34, 197, 94, 0.3)' // 绿色中心
}, {
offset: 1,
color: 'rgba(34, 197, 94, 0.8)' // 绿色边缘
}]
} : {
type: 'radial',
x: 0.5,
y: 0.5,
r: 0.5,
colorStops: [{
offset: 0,
color: 'rgba(189, 181, 255, 0.3)' // 紫色中心
}, {
offset: 1,
color: 'rgba(189, 181, 255, 0.8)' // 紫色边缘
}]
};
// 为指标添加分数显示
const indicatorWithScore = indicator.map((item, index) => ({
...item,
name: item.name,
value: actualData[index] // 存储分数值
}));
const option = {
tooltip: { show: false },
grid: { show: false },
radar: [
{
center: ["50%", "50%"],
indicator,
radius: "55%", // 调小半径以确保文字不超出容器
nameGap: 8, // 调整文字与雷达图的间距
indicator: indicatorWithScore,
radius: "45%", // 调小半径以确保文字和分数不超出容器
nameGap: 35, // 增大间距,让文字距离雷达图更远
shape: "circle", // 设置雷达图外圈为圆形
// 网格线样式配置
splitLine: {
@@ -41,17 +79,40 @@ export default function RadarChart({
color: "#4E5969",
fontSize: 12,
fontWeight: "900", // 更粗的文字
lineHeight: 16,
formatter: function(value) {
// 已经包含换行符的直接返回
lineHeight: 20,
align: 'center', // 文本居中对齐
rich: {
score: {
fontSize: 18,
fontWeight: 'bold',
color: isGreenTheme ? '#22c55e' : '#8b5cf6',
padding: [0, 0, 8, 0],
align: 'center',
lineHeight: 24
},
name: {
fontSize: 12,
color: '#4E5969',
fontWeight: '900',
align: 'center',
lineHeight: 16
}
},
formatter: function(value, indicator) {
const index = indicatorWithScore.findIndex(item => item.name === value);
const score = actualData[index];
// 已经包含换行符的处理
if (value.includes('\n')) {
return value;
const parts = value.split('\n');
return '{score|' + score + '}\n{name|' + parts.join('\n') + '}';
}
// 对较长的文本进行换行处理
if (value.length > 8) {
return value.substring(0, 8) + '\n' + value.substring(8);
const line1 = value.substring(0, 8);
const line2 = value.substring(8);
return '{score|' + score + '}\n{name|' + line1 + '\n' + line2 + '}';
}
return value;
return '{score|' + score + '}\n{name|' + value + '}';
},
},
axisLine: {
@@ -78,10 +139,10 @@ export default function RadarChart({
color: "#333",
},
areaStyle: {
color: areaColor, // 面积填充颜
color: gradientColor, // 使用渐变
}, // 关键:显示面积
lineStyle: {
color: areaBorderColor, // 连接线颜色(边框颜色)
color: isGreenTheme ? '#22c55e' : areaBorderColor, // 连接线颜色(边框颜色)
width: 3, // 连接线宽度
type: "solid", // 线条类型
},
@@ -89,7 +150,7 @@ export default function RadarChart({
symbolSize: 6, // 端点大小
itemStyle: {
color: "#fff", // 端点颜色
borderColor: areaColor, // 端点边框颜色
borderColor: isGreenTheme ? '#22c55e' : areaBorderColor, // 端点边框颜色
borderWidth: 2, // 端点边框宽度
},
}],

View File

@@ -69,7 +69,8 @@ const ScoreChart = ({
lineHeight: 40,
borderRadius: 8,
offsetCenter: [0, "-15%"],
fontSize: 24,
fontSize: 36,
fontWeight: "bold",
formatter: "{value}.00 ",
color: "#1D2129",
},