简历推荐功能开发
Some checks failed
Node CI / build (14.x, macOS-latest) (push) Has been cancelled
Node CI / build (14.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (14.x, windows-latest) (push) Has been cancelled
Node CI / build (16.x, macOS-latest) (push) Has been cancelled
Node CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (16.x, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
coverage CI / build (push) Has been cancelled
Node pnpm CI / build (16.x, macOS-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, windows-latest) (push) Has been cancelled

This commit is contained in:
冯辉
2026-06-27 17:57:45 +08:00
parent f248d9a409
commit e13cea8c36
9 changed files with 1000 additions and 143 deletions

View File

@@ -0,0 +1,206 @@
import { Modal, Card, Form, Select, Input, DatePicker, Button, message } from 'antd';
import React, { useEffect, useState } from 'react';
import { addCmsInterview, getCmsInterviewList } from '@/services/Management/list';
import dayjs from 'dayjs';
const { TextArea } = Input;
const { Option } = Select;
export type InterviewInviteProps = {
onClose: (needRefresh?: boolean) => void;
open: boolean;
userId?: number;
userName?: string;
jobId?: string;
companyId?: number;
jobName?: string;
};
const InterviewInvite: React.FC<InterviewInviteProps> = (props) => {
const { open, userId, jobId, companyId, jobName } = props;
const [interviewForm] = Form.useForm();
const [interviewMethod, setInterviewMethod] = useState<string>('online');
const [submitting, setSubmitting] = useState<boolean>(false);
const [alreadyInvited, setAlreadyInvited] = useState<boolean>(false);
const [checkingInvitation, setCheckingInvitation] = useState<boolean>(false);
useEffect(() => {
if (open && userId && jobId) {
checkExistingInvitation();
interviewForm.resetFields();
setInterviewMethod('online');
}
}, [open, userId, jobId]);
// 检查是否已存在有效邀约
const checkExistingInvitation = async () => {
if (!jobId || !userId) return;
setCheckingInvitation(true);
try {
const res = await getCmsInterviewList({
jobId: Number(jobId),
userId: userId,
});
if (res?.code === 200 && res?.rows?.length > 0) {
const hasActive = res.rows.some(
(item: API.Management.InterviewInvitation) => item.status !== 'rejected'
);
setAlreadyInvited(hasActive);
} else {
setAlreadyInvited(false);
}
} catch (error) {
console.error('检查邀约状态失败:', error);
} finally {
setCheckingInvitation(false);
}
};
// 发送面试邀约
const handleInterviewSubmit = async () => {
try {
const values = await interviewForm.validateFields();
setSubmitting(true);
const payload: API.Management.InterviewInvitation = {
companyId: companyId,
jobId: Number(jobId),
userId: userId,
jobName: jobName,
interviewTime: values.interviewTime
? dayjs(values.interviewTime).format('YYYY-MM-DD HH:mm:ss')
: undefined,
interviewMethod: values.interviewMethod,
meetingLink: values.meetingLink,
meetingPassword: values.meetingPassword,
interviewLocation: values.interviewLocation,
contactPhone: values.contactPhone,
interviewerName: values.interviewerName,
status: 'pending',
remark: values.remark,
};
const response = await addCmsInterview(payload);
if (response.code === 200) {
message.success('面试邀约已发送');
setAlreadyInvited(true);
interviewForm.resetFields();
setInterviewMethod('online');
} else {
message.error(response.msg || '发送失败');
}
} catch (error: any) {
const errMsg = error?.response?.data?.msg || error?.message || '发送面试邀约失败';
message.error(errMsg);
console.error('发送面试邀约失败:', error);
} finally {
setSubmitting(false);
}
};
return (
<Modal
open={open}
onCancel={() => props.onClose()}
title="邀请面试"
width={700}
footer={[
<Button key="cancel" onClick={() => props.onClose()}>
</Button>,
]}
destroyOnClose
>
<Card title="面试邀约" bordered>
<Form
form={interviewForm}
layout="vertical"
initialValues={{ interviewMethod: 'online', meetingLink: 'https://mc.repohub.cn/pro.html' }}
>
<Form.Item
name="interviewMethod"
label="面试方式"
rules={[{ required: true, message: '请选择面试方式' }]}
>
<Select
placeholder="请选择面试方式"
onChange={(value) => setInterviewMethod(value)}
>
<Option value="online">线</Option>
<Option value="offline">线</Option>
</Select>
</Form.Item>
<Form.Item
name="interviewTime"
label="面试时间"
rules={[{ required: true, message: '请选择面试时间' }]}
>
<DatePicker
showTime
style={{ width: '100%' }}
placeholder="请选择面试时间"
/>
</Form.Item>
{interviewMethod === 'online' && (
<Form.Item
name="meetingLink"
label="在线会议链接"
>
<Input placeholder="请输入在线会议链接" />
</Form.Item>
)}
{interviewMethod === 'offline' && (
<Form.Item
name="interviewLocation"
label="面试地点"
rules={[{ required: true, message: '请输入面试地点' }]}
>
<Input placeholder="请输入面试地点" />
</Form.Item>
)}
<Form.Item
name="remark"
label="备注"
>
<TextArea rows={3} placeholder="选填,如有其他注意事项可在此说明" />
</Form.Item>
<Form.Item
name="interviewerName"
label="面试官姓名"
rules={[{ required: true, message: '请输入面试官姓名' }]}
>
<Input placeholder="请输入面试官姓名" />
</Form.Item>
<Form.Item
name="contactPhone"
label="面试官联系方式"
rules={[{ required: true, message: '请输入面试官联系方式' }]}
>
<Input placeholder="请输入手机号或微信等联系方式" />
</Form.Item>
<Form.Item>
<Button
type="primary"
loading={submitting || checkingInvitation}
disabled={alreadyInvited}
onClick={handleInterviewSubmit}
block
>
{alreadyInvited ? '已邀约' : '发送面试邀约'}
</Button>
</Form.Item>
</Form>
</Card>
</Modal>
);
};
export default InterviewInvite;

View File

@@ -0,0 +1,211 @@
import { Modal, Descriptions, List, Card, Tag, Empty, Spin } from 'antd';
import React, { useEffect, useState } from 'react';
import { addCmsUserWorkExperiencesList, getAppSkillList } from '@/services/Management/list';
export type ResumeDetailProps = {
onClose: (flag?: boolean) => void;
open: boolean;
values?: API.ManagementList.RecommendUser;
jobId?: string;
};
const ResumeDetail: React.FC<ResumeDetailProps> = (props) => {
const { values, jobId } = props;
const [loading, setLoading] = useState(false);
const [experienceList, setExperienceList] = useState<API.ManagementList.RecommendExperience[]>([]);
const [skillList, setSkillList] = useState<API.ManagementList.RecommendSkill[]>([]);
// 性别映射
const sexMap: Record<string, string> = {
'0': '男',
'1': '女',
};
// 学历映射(常用值)
const educationMap: Record<string, string> = {
'1': '初中及以下',
'2': '高中',
'3': '中专',
'4': '大专',
'5': '本科',
'6': '硕士',
'7': '博士',
};
// 政治面貌映射
const politicalMap: Record<string, string> = {
'1': '中共党员',
'2': '中共预备党员',
'3': '共青团员',
'4': '群众',
'5': '民主党派',
};
// 弹窗打开时获取工作经历和技能
useEffect(() => {
if (props.open && values?.userId) {
fetchWorkExperiences();
fetchSkills();
} else {
setExperienceList([]);
setSkillList([]);
}
}, [props.open, values?.userId]);
const fetchWorkExperiences = async () => {
if (!values?.userId) return;
setLoading(true);
try {
const params: any = { userId: values.userId };
if (jobId) {
params.jobId = Number(jobId);
}
const response = await addCmsUserWorkExperiencesList(params);
if (response.code === 200) {
setExperienceList(response.rows || []);
} else {
setExperienceList(values.experiencesList || []);
}
} catch (error) {
console.error('获取工作经历失败:', error);
setExperienceList(values.experiencesList || []);
} finally {
setLoading(false);
}
};
const fetchSkills = async () => {
if (!values?.userId) return;
try {
const response = await getAppSkillList({ userId: values.userId });
if (response.code === 200) {
setSkillList(response.rows || []);
} else {
setSkillList(values.appSkillsList || []);
}
} catch (error) {
console.error('获取技能信息失败:', error);
setSkillList(values.appSkillsList || []);
}
};
return (
<Modal
open={props.open}
onCancel={() => props.onClose()}
title="简历详情"
width={900}
footer={null}
destroyOnClose
>
{!values ? (
<Empty description="暂无数据" />
) : (
<>
{/* 基本信息 */}
<Card title="基本信息" bordered style={{ marginBottom: 16 }}>
<Descriptions column={3} size="small" bordered>
<Descriptions.Item label="姓名">{values.name || '-'}</Descriptions.Item>
<Descriptions.Item label="性别">
{sexMap[values.sex || ''] || values.sex || '-'}
</Descriptions.Item>
<Descriptions.Item label="出生日期">{values.birthDate || '-'}</Descriptions.Item>
<Descriptions.Item label="手机号码">{values.phone || '-'}</Descriptions.Item>
<Descriptions.Item label="学历">
{educationMap[values.education || ''] || values.education || '-'}
</Descriptions.Item>
<Descriptions.Item label="政治面貌">
{politicalMap[values.politicalAffiliation || ''] || values.politicalAffiliation || '-'}
</Descriptions.Item>
<Descriptions.Item label="期望薪资">
{values.salaryMin && values.salaryMax
? `${values.salaryMin}-${values.salaryMax}`
: '-'}
</Descriptions.Item>
<Descriptions.Item label="身份证号">{values.idCard || '-'}</Descriptions.Item>
<Descriptions.Item label="民族">{values.nation || '-'}</Descriptions.Item>
</Descriptions>
</Card>
{/* 匹配级别 */}
{values.matchLevelDesc && (
<Card title="匹配信息" bordered style={{ marginBottom: 16 }}>
<Tag color="blue" style={{ fontSize: 14, padding: '4px 12px' }}>
{values.matchLevelDesc}
</Tag>
</Card>
)}
{/* 技能列表 */}
<Card title="技能信息" bordered style={{ marginBottom: 16 }}>
<Spin spinning={loading}>
{!skillList || skillList.length === 0 ? (
<Empty description="暂无技能信息" />
) : (
<List
itemLayout="horizontal"
dataSource={skillList}
renderItem={(skill: API.ManagementList.RecommendSkill) => (
<List.Item>
<List.Item.Meta
title={skill.name || '未命名技能'}
description={`等级: ${skill.levels || '未填写'}`}
/>
</List.Item>
)}
/>
)}
</Spin>
</Card>
{/* 工作经历 */}
<Card title="工作经历" bordered style={{ marginBottom: 16 }}>
<Spin spinning={loading}>
{!experienceList || experienceList.length === 0 ? (
<Empty description="暂无工作经历" />
) : (
<List
itemLayout="vertical"
dataSource={experienceList}
renderItem={(item: API.ManagementList.RecommendExperience, index: number) => (
<List.Item style={{ marginBottom: 12 }}>
<Card
title={`经历 #${index + 1}`}
bordered
size="small"
style={{ width: '100%', boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}
>
<Descriptions column={2} size="small">
<Descriptions.Item label="公司名称">
{item.companyName || '-'}
</Descriptions.Item>
<Descriptions.Item label="职务">
{item.position || '-'}
</Descriptions.Item>
<Descriptions.Item label="开始时间">
{item.startDate || '-'}
</Descriptions.Item>
<Descriptions.Item label="截止时间">
{item.endDate || '-'}
</Descriptions.Item>
<Descriptions.Item label="工作描述" span={2}>
{item.description || '-'}
</Descriptions.Item>
</Descriptions>
</Card>
</List.Item>
)}
/>
)}
</Spin>
</Card>
</>
)}
</Modal>
);
};
export default ResumeDetail;

View File

@@ -0,0 +1,247 @@
import React, { Fragment, useEffect, useRef, useState } from 'react';
import { useAccess, useParams } from '@umijs/max';
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
import { Button, message } from 'antd';
import { AlignLeftOutlined, SendOutlined } from '@ant-design/icons';
import { getDictValueEnum } from '@/services/system/dict';
import DictTag from '@/components/DictTag';
import { getRecommendByJobId, getCmsJobIds } from '@/services/Management/list';
import ResumeDetail from './ResumeDetail';
import InterviewInvite from './InterviewInvite';
function ResumeRecommend() {
const access = useAccess();
const actionRef = useRef<ActionType>();
const [educationEnum, setEducationEnum] = useState<any>([]);
const [areaEnum, setAreaEnum] = useState<any>([]);
const [sexEnum, setSexEnum] = useState<any>([]);
const [politicalEnum, setPoliticalEnum] = useState<any>([]);
const [currentRow, setCurrentRow] = useState<API.ManagementList.RecommendUser>();
const [detailVisible, setDetailVisible] = useState<boolean>(false);
const [inviteVisible, setInviteVisible] = useState<boolean>(false);
const [jobInfo, setJobInfo] = useState<any>({});
const params = useParams();
const jobId = params.id || '0';
useEffect(() => {
getDictValueEnum('education', true, true).then((data) => {
setEducationEnum(data);
});
getDictValueEnum('area', true, true).then((data) => {
setAreaEnum(data);
});
getDictValueEnum('sys_user_sex', true).then((data) => {
setSexEnum(data);
});
getDictValueEnum('political_affiliation', true, true).then((data) => {
setPoliticalEnum(data);
});
getJobInfo(jobId);
}, [jobId]);
const getJobInfo = async (id: string) => {
try {
const resData = await getCmsJobIds(id);
if (resData.code === 200 && resData.data) {
const data = resData.data as any;
setJobInfo({
jobTitle: data.jobTitle || '',
companyId: data.companyId,
});
}
} catch (error) {
console.error('获取岗位信息失败:', error);
}
};
const columns: ProColumns<API.ManagementList.RecommendUser>[] = [
{
title: '用户名',
dataIndex: 'name',
valueType: 'text',
align: 'center',
},
{
title: '期望薪资',
dataIndex: 'minSalary',
valueType: 'text',
hideInSearch: true,
align: 'center',
render: (_, record) => (
<>
{record.salaryMin}-{record.salaryMax}
</>
),
},
{
title: '出生日期',
dataIndex: 'birthDate',
valueType: 'text',
align: 'center',
hideInSearch: true,
},
{
title: '学历要求',
dataIndex: 'education',
valueType: 'select',
align: 'center',
valueEnum: educationEnum,
render: (_, record) => {
return <DictTag enums={educationEnum} value={record.education} />;
},
},
{
title: '区域',
dataIndex: 'area',
valueType: 'select',
align: 'center',
valueEnum: areaEnum,
render: (_, record) => {
return <DictTag enums={areaEnum} value={record.area} />;
},
},
{
title: '性别',
dataIndex: 'sex',
valueType: 'select',
align: 'center',
valueEnum: sexEnum,
render: (_, record) => {
return <DictTag enums={sexEnum} value={record.sex} />;
},
},
{
title: '政治面貌',
dataIndex: 'politicalAffiliation',
valueType: 'select',
align: 'center',
valueEnum: politicalEnum,
render: (_, record) => {
return <DictTag enums={politicalEnum} value={record.politicalAffiliation} />;
},
},
{
title: '手机号码',
dataIndex: 'phone',
valueType: 'text',
align: 'center',
hideInSearch: true,
},
{
title: '匹配级别',
dataIndex: 'matchLevelDesc',
valueType: 'text',
align: 'center',
hideInSearch: true,
width: 200,
},
{
title: '操作',
hideInSearch: true,
align: 'center',
dataIndex: 'userId',
width: 200,
render: (_, record) => [
<div key="actions" style={{ display: 'flex', justifyContent: 'center', gap: 8 }}>
<Button
type="link"
size="small"
key="view-resume"
icon={<AlignLeftOutlined />}
onClick={() => {
setCurrentRow(record);
setDetailVisible(true);
}}
>
</Button>
<Button
type="link"
size="small"
key="invite-interview"
icon={<SendOutlined />}
hidden={!access.hasPerms('cms:interview:add')}
onClick={() => {
setCurrentRow(record);
setInviteVisible(true);
}}
>
</Button>
</div>,
],
},
];
return (
<Fragment>
<div style={{ width: '100%', float: 'right' }}>
<ProTable<API.ManagementList.RecommendUser>
actionRef={actionRef}
rowKey="userId"
key="index"
headerTitle={`简历推荐${jobInfo.jobTitle ? ` - ${jobInfo.jobTitle}` : ''}`}
columns={columns}
request={async () => {
try {
const res = await getRecommendByJobId(jobId);
if (res.code === 200) {
const data = res.data || [];
return {
data,
total: data.length,
success: true,
};
} else {
message.error(res.msg || '获取推荐简历失败');
return {
data: [],
total: 0,
success: false,
};
}
} catch (error) {
message.error('获取推荐简历失败');
return {
data: [],
total: 0,
success: false,
};
}
}}
search={false}
pagination={{ pageSize: 10 }}
/>
</div>
<ResumeDetail
values={currentRow}
open={detailVisible}
jobId={jobId}
onClose={() => {
setDetailVisible(false);
setCurrentRow(undefined);
}}
/>
<InterviewInvite
open={inviteVisible}
userId={currentRow?.userId}
userName={currentRow?.name}
jobId={jobId}
companyId={jobInfo.companyId}
jobName={jobInfo.jobTitle}
onClose={(needRefresh) => {
setInviteVisible(false);
setCurrentRow(undefined);
if (needRefresh) {
actionRef.current?.reload();
}
}}
/>
</Fragment>
);
}
export default ResumeRecommend;

View File

@@ -2,7 +2,7 @@ import React, { Fragment, useEffect, useRef, useState } from 'react';
import { FormattedMessage, useAccess, useParams } from '@umijs/max';
import { Button, FormInstance, message, Tag } from 'antd';
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
import { FormOutlined, PlusOutlined,AlignLeftOutlined } from '@ant-design/icons';
import { FormOutlined, PlusOutlined, AlignLeftOutlined, SendOutlined } from '@ant-design/icons';
import { getDictValueEnum } from '@/services/system/dict';
import DictTag from '@/components/DictTag';
import { exportCmsAppUserExport } from '@/services/mobileusers/list';
@@ -15,6 +15,7 @@ import similarityJobs from '@/utils/similarity_Job';
import Detail from './detail';
import Hire from './hire';
import ResumeView from './resumeView';
import InterviewInvite from '../ResumeRecommend/InterviewInvite';
const handleExport = async (values: API.MobileUser.ListParams) => {
@@ -51,6 +52,7 @@ function ManagementList() {
const [jobInfo, setJobInfo] = useState({});
const [matchingDegree, setMatchingDegree] = useState<any>(null);
const [resumeVisible, setResumeVisible] = useState<boolean>(false);
const [inviteVisible, setInviteVisible] = useState<boolean>(false);
const [hireAdd, setHireAdd] = useState<any>(null);
const [mode, setMode] = useState<'view' | 'edit' | 'create'>('create');
const params = useParams();
@@ -191,7 +193,7 @@ function ManagementList() {
hideInSearch: true,
align: 'center',
dataIndex: 'jobId',
width: 200,
width: 280,
render: (jobId, record) => [
<div key="first-row" style={{marginBottom: 8, display: 'flex', justifyContent: 'center'}}>
<Button
@@ -210,7 +212,20 @@ function ManagementList() {
<Button
type="link"
size="small"
key="edit"
key="invite-interview"
icon={<SendOutlined/>}
hidden={!access.hasPerms('cms:interview:add')}
onClick={() => {
setCurrentRow(record);
setInviteVisible(true);
}}
>
</Button>
<Button
type="link"
size="small"
key="hire"
icon={<FormOutlined/>}
hidden={!access.hasPerms('cms:employeeConfirm:add')}
onClick={() => {
@@ -224,9 +239,8 @@ function ManagementList() {
<Button
type="link"
size="small"
key="edit"
key="match-detail"
icon={<AlignLeftOutlined/>}
// hidden={!access.hasPerms('area:business:List.update')}
onClick={() => {
setModalVisible(true);
setCurrentRow(record);
@@ -323,6 +337,21 @@ function ManagementList() {
setCurrentRow(undefined);
}}
/>
<InterviewInvite
open={inviteVisible}
userId={currentRow?.userId}
userName={currentRow?.name}
jobId={jobId}
companyId={(jobInfo as any)?.companyId}
jobName={(jobInfo as any)?.jobTitle}
onClose={(needRefresh) => {
setInviteVisible(false);
setCurrentRow(undefined);
if (needRefresh) {
actionRef.current?.reload();
}
}}
/>
</div>
</Fragment>
);

View File

@@ -1,10 +1,10 @@
import {Modal, Card, Descriptions, List, Button, Divider, Form, Select, Input, DatePicker, message} from 'antd';
import React, { useEffect, useRef,useState } from 'react';
import { ProDescriptions} from '@ant-design/pro-components';
import { Modal, Card, Descriptions, List, Button, Form, Select, Input, DatePicker, message, Empty, Spin } from 'antd';
import React, { useEffect, useState } from 'react';
import {
addCmsUserWorkExperiencesList,
addCmsInterview,
getCmsInterviewList,
getAppSkillList,
} from '@/services/Management/list';
import dayjs from 'dayjs';
@@ -18,11 +18,11 @@ export type ListFormProps = {
matching?: any;
};
const listEdit: React.FC<ListFormProps> = (props) => {
const ResumeViewModal: React.FC<ListFormProps> = (props) => {
const mapRef = useRef(null);
const [loading, setLoading] = useState(false);
const [contactInfoList, setContactInfoList] = useState<API.Management.WorkExperence[]>([]);
const [experienceList, setExperienceList] = useState<any[]>([]);
const [skillList, setSkillList] = useState<any[]>([]);
// 面试邀约相关状态
const [interviewForm] = Form.useForm();
@@ -31,16 +31,41 @@ const listEdit: React.FC<ListFormProps> = (props) => {
const [alreadyInvited, setAlreadyInvited] = useState<boolean>(false);
const [checkingInvitation, setCheckingInvitation] = useState<boolean>(false);
// 性别映射
const sexMap: Record<string, string> = {
'0': '男',
'1': '女',
};
// 学历映射
const educationMap: Record<string, string> = {
'1': '初中及以下',
'2': '高中',
'3': '中专',
'4': '大专',
'5': '本科',
'6': '硕士',
'7': '博士',
};
// 政治面貌映射
const politicalMap: Record<string, string> = {
'1': '中共党员',
'2': '中共预备党员',
'3': '共青团员',
'4': '群众',
'5': '民主党派',
};
useEffect(() => {
if (props.open && props.values) {
console.log(props.values);
fetchResumeDetail();
fetchSkills();
checkExistingInvitation();
// 重置面试邀约表单
interviewForm.resetFields();
setInterviewMethod('online');
}
}, [props.values,props.open]);
}, [props.values, props.open]);
// 检查是否已存在有效邀约
const checkExistingInvitation = async () => {
@@ -52,7 +77,6 @@ const listEdit: React.FC<ListFormProps> = (props) => {
userId: props.values.userId,
});
if (res?.code === 200 && res?.rows?.length > 0) {
// 存在非 rejected 的邀约即为已邀约
const hasActive = res.rows.some(
(item: API.Management.InterviewInvitation) => item.status !== 'rejected'
);
@@ -67,13 +91,12 @@ const listEdit: React.FC<ListFormProps> = (props) => {
const fetchResumeDetail = async () => {
if (!props.values?.userId) return;
setLoading(true);
try {
const parm={userId: props.values.userId, jobId: props.matching?.jobId};
const parm = { userId: props.values.userId, jobId: props.matching?.jobId };
const response = await addCmsUserWorkExperiencesList(parm);
if (response.code === 200) {
setContactInfoList(response.rows);
setExperienceList(response.rows);
}
} catch (error) {
console.error('获取经历详情失败:', error);
@@ -82,6 +105,18 @@ const listEdit: React.FC<ListFormProps> = (props) => {
}
};
const fetchSkills = async () => {
if (!props.values?.userId) return;
try {
const response = await getAppSkillList({ userId: props.values.userId });
if (response.code === 200) {
setSkillList(response.rows || []);
}
} catch (error) {
console.error('获取技能信息失败:', error);
}
};
// 发送面试邀约
const handleInterviewSubmit = async () => {
try {
@@ -125,162 +160,200 @@ const listEdit: React.FC<ListFormProps> = (props) => {
}
};
const values = props.values;
return (
<Modal
open={props.open}
onCancel={() => props.onClose()}
title="简历详情"
width={800}
width={900}
footer={[
<Button key="cancel" onClick={() => props.onClose()}>
</Button>
</Button>,
]}
destroyOnClose
>
<ProDescriptions
column={2}
loading={loading}
>
{
<ProDescriptions.Item
dataIndex="contactInfoList"
label=""
span={2}
render={() => {
if (loading) return '加载中...';
if (!Array.isArray(contactInfoList) || contactInfoList.length === 0) {
return <span style={{ color: '#999' }}></span>;
}
return (
{!values ? (
<Empty description="暂无数据" />
) : (
<>
{/* 基本信息 */}
<Card title="基本信息" bordered style={{ marginBottom: 16 }}>
<Descriptions column={3} size="small" bordered>
<Descriptions.Item label="姓名">{values.name || '-'}</Descriptions.Item>
<Descriptions.Item label="性别">
{sexMap[values.sex || ''] || values.sex || '-'}
</Descriptions.Item>
<Descriptions.Item label="出生日期">{values.birthDate || '-'}</Descriptions.Item>
<Descriptions.Item label="手机号码">{values.phone || '-'}</Descriptions.Item>
<Descriptions.Item label="学历">
{educationMap[values.education || ''] || values.education || '-'}
</Descriptions.Item>
<Descriptions.Item label="政治面貌">
{politicalMap[values.politicalAffiliation || ''] || values.politicalAffiliation || '-'}
</Descriptions.Item>
<Descriptions.Item label="期望薪资">
{values.salaryMin && values.salaryMax
? `${values.salaryMin}-${values.salaryMax}`
: '-'}
</Descriptions.Item>
<Descriptions.Item label="身份证号">{values.idCard || '-'}</Descriptions.Item>
</Descriptions>
</Card>
{/* 技能列表 */}
{skillList.length > 0 && (
<Card title="技能信息" bordered style={{ marginBottom: 16 }}>
<List
itemLayout="horizontal"
dataSource={skillList}
renderItem={(skill: any) => (
<List.Item>
<List.Item.Meta
title={skill.name || '未命名技能'}
description={`等级: ${skill.levels || '未填写'}`}
/>
</List.Item>
)}
/>
</Card>
)}
{/* 工作经历 */}
<Card title="工作经历" bordered style={{ marginBottom: 16 }}>
<Spin spinning={loading}>
{!experienceList || experienceList.length === 0 ? (
<Empty description="暂无工作经历" />
) : (
<List
itemLayout="vertical"
dataSource={contactInfoList}
renderItem={(item, index) => (
<List.Item
style={{ marginBottom: 16 }}
>
dataSource={experienceList}
renderItem={(item: any, index: number) => (
<List.Item style={{ marginBottom: 12 }}>
<Card
title={`经历信息 #${index + 1}`}
title={`经历 #${index + 1}`}
bordered
style={{ width: '100%', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}
size="small"
style={{ width: '100%', boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}
>
<Descriptions column={2} size="small">
<Descriptions.Item label="公司名称">{item.companyName || '未填写'}</Descriptions.Item>
<Descriptions.Item label="职务">{item.position || '未填写'}</Descriptions.Item>
<Descriptions.Item label="开始时间">{item.startDate || '未填写'}</Descriptions.Item>
<Descriptions.Item label="截止时间">{item.endDate || '未填写'}</Descriptions.Item>
<Descriptions.Item label="描述" span={2}>
{item.description || '未填写'}
<Descriptions.Item label="公司名称">
{item.companyName || '-'}
</Descriptions.Item>
<Descriptions.Item label="职务">
{item.position || '-'}
</Descriptions.Item>
<Descriptions.Item label="开始时间">
{item.startDate || '-'}
</Descriptions.Item>
<Descriptions.Item label="截止时间">
{item.endDate || '-'}
</Descriptions.Item>
<Descriptions.Item label="工作描述" span={2}>
{item.description || '-'}
</Descriptions.Item>
</Descriptions>
</Card>
</List.Item>
)}
/>
);
}}
/>
}
</ProDescriptions>
)}
</Spin>
</Card>
<Divider />
<Card title="面试邀约" bordered style={{ marginTop: 16 }}>
<Form
form={interviewForm}
layout="vertical"
initialValues={{ interviewMethod: 'online', meetingLink: 'https://mc.repohub.cn/pro.html' }}
>
<Form.Item
name="interviewMethod"
label="面试方式"
rules={[{ required: true, message: '请选择面试方式' }]}
>
<Select
placeholder="请选择面试方式"
onChange={(value) => setInterviewMethod(value)}
{/* 面试邀约 */}
<Card title="面试邀约" bordered>
<Form
form={interviewForm}
layout="vertical"
initialValues={{ interviewMethod: 'online', meetingLink: 'https://mc.repohub.cn/pro.html' }}
>
<Option value="online">线</Option>
<Option value="offline">线</Option>
</Select>
</Form.Item>
<Form.Item
name="interviewTime"
label="面试时间"
rules={[{ required: true, message: '请选择面试时间' }]}
>
<DatePicker
showTime
style={{ width: '100%' }}
placeholder="请选择面试时间"
/>
</Form.Item>
{interviewMethod === 'online' && (
<>
<Form.Item
name="meetingLink"
label="在线会议链接"
name="interviewMethod"
label="面试方式"
rules={[{ required: true, message: '请选择面试方式' }]}
>
<Input placeholder="https://mc.repohub.cn/pro.html" disabled />
<Select
placeholder="请选择面试方式"
onChange={(value) => setInterviewMethod(value)}
>
<Option value="online">线</Option>
<Option value="offline">线</Option>
</Select>
</Form.Item>
{/* <Form.Item
name="meetingPassword"
label="会议密码"
rules={[{ required: true, message: '请输入会议密码' }]}
<Form.Item
name="interviewTime"
label="面试时间"
rules={[{ required: true, message: '请选择面试时间' }]}
>
<Input placeholder="请输入会议密码" />
</Form.Item> */}
</>
)}
<DatePicker
showTime
style={{ width: '100%' }}
placeholder="请选择面试时间"
/>
</Form.Item>
{interviewMethod === 'offline' && (
<Form.Item
name="interviewLocation"
label="面试地点"
rules={[{ required: true, message: '请输入面试地点' }]}
>
<Input placeholder="请输入面试地点" />
</Form.Item>
)}
{interviewMethod === 'online' && (
<Form.Item
name="meetingLink"
label="在线会议链接"
>
<Input placeholder="请输入在线会议链接" />
</Form.Item>
)}
<Form.Item
name="remark"
label="备注"
>
<TextArea rows={3} placeholder="选填,如有其他注意事项可在此说明" />
</Form.Item>
{interviewMethod === 'offline' && (
<Form.Item
name="interviewLocation"
label="面试地点"
rules={[{ required: true, message: '请输入面试地点' }]}
>
<Input placeholder="请输入面试地点" />
</Form.Item>
)}
<Form.Item
name="interviewerName"
label="面试官姓名"
rules={[{ required: true, message: '请输入面试官姓名' }]}
>
<Input placeholder="请输入面试官姓名" />
</Form.Item>
<Form.Item
name="remark"
label="备注"
>
<TextArea rows={3} placeholder="选填,如有其他注意事项可在此说明" />
</Form.Item>
<Form.Item
name="contactPhone"
label="面试官联系方式"
rules={[{ required: true, message: '请输入面试官联系方式' }]}
>
<Input placeholder="请输入手机号或微信等联系方式" />
</Form.Item>
<Form.Item
name="interviewerName"
label="面试官姓名"
rules={[{ required: true, message: '请输入面试官姓名' }]}
>
<Input placeholder="请输入面试官姓名" />
</Form.Item>
<Form.Item>
<Button
type="primary"
loading={submitting || checkingInvitation}
disabled={alreadyInvited}
onClick={handleInterviewSubmit}
>
{alreadyInvited ? '已邀约' : '发送面试邀约'}
</Button>
</Form.Item>
</Form>
</Card>
<Form.Item
name="contactPhone"
label="面试官联系方式"
rules={[{ required: true, message: '请输入面试官联系方式' }]}
>
<Input placeholder="请输入手机号或微信等联系方式" />
</Form.Item>
<Form.Item>
<Button
type="primary"
loading={submitting || checkingInvitation}
disabled={alreadyInvited}
onClick={handleInterviewSubmit}
>
{alreadyInvited ? '已邀约' : '发送面试邀约'}
</Button>
</Form.Item>
</Form>
</Card>
</>
)}
</Modal>
);
};
export default listEdit;
export default ResumeViewModal;

View File

@@ -12,7 +12,7 @@ import {
} from '@/services/Management/list';
import { Button, FormInstance, message, Modal, Switch } from 'antd';
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
import { AlignLeftOutlined, BarChartOutlined, DeleteOutlined, FormOutlined, PlusOutlined, DownloadOutlined, UploadOutlined } from '@ant-design/icons';
import { AlignLeftOutlined, BarChartOutlined, DeleteOutlined, FormOutlined, PlusOutlined, DownloadOutlined, UploadOutlined, IdcardOutlined } from '@ant-design/icons';
import EditManageRow from './edit';
import { getDictValueEnum } from '@/services/system/dict';
import DictTag from '@/components/DictTag';
@@ -307,7 +307,7 @@ function ManagementList() {
hideInSearch: true,
align: 'center',
dataIndex: 'jobId',
width: 300,
width: 380,
render: (jobId, record) => [
<div key="first-row" style={{ marginBottom: 8, display: 'flex', justifyContent: 'center' }}>
<Button
@@ -327,13 +327,23 @@ function ManagementList() {
<Button
type="link"
size="small"
key="edit"
key="candidates"
icon={<BarChartOutlined />}
hidden={!access.hasPerms('bussiness:job:candidates')}
onClick={() => history.push(`/management/see-matching/index/${record.jobId}`)}
>
</Button>
<Button
type="link"
size="small"
key="recommend"
icon={<IdcardOutlined />}
hidden={!access.hasPerms('bussiness:job:candidates')}
onClick={() => history.push(`/management/resume-recommend/index/${record.jobId}`)}
>
</Button>
</div>,
<div key="second-row" style={{ display: 'flex', justifyContent: 'space-evenly'}}>
<Button