2026-06-27 17:57:45 +08:00
|
|
|
import { Modal, Card, Descriptions, List, Button, Form, Select, Input, DatePicker, message, Empty, Spin } from 'antd';
|
|
|
|
|
import React, { useEffect, useState } from 'react';
|
2025-11-10 16:28:01 +08:00
|
|
|
import {
|
2026-06-24 09:31:10 +08:00
|
|
|
addCmsUserWorkExperiencesList,
|
|
|
|
|
addCmsInterview,
|
2026-06-24 18:18:42 +08:00
|
|
|
getCmsInterviewList,
|
2026-06-27 17:57:45 +08:00
|
|
|
getAppSkillList,
|
2025-11-10 16:28:01 +08:00
|
|
|
} from '@/services/Management/list';
|
2026-06-24 09:31:10 +08:00
|
|
|
import dayjs from 'dayjs';
|
|
|
|
|
|
|
|
|
|
const { TextArea } = Input;
|
|
|
|
|
const { Option } = Select;
|
|
|
|
|
|
2025-11-10 16:28:01 +08:00
|
|
|
export type ListFormProps = {
|
|
|
|
|
onClose: (flag?: boolean, formVals?: unknown) => void;
|
|
|
|
|
open: boolean;
|
|
|
|
|
values?: Partial<API.MobileUser.ListRow>;
|
|
|
|
|
matching?: any;
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-27 17:57:45 +08:00
|
|
|
const ResumeViewModal: React.FC<ListFormProps> = (props) => {
|
2025-11-10 16:28:01 +08:00
|
|
|
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
2026-06-27 17:57:45 +08:00
|
|
|
const [experienceList, setExperienceList] = useState<any[]>([]);
|
|
|
|
|
const [skillList, setSkillList] = useState<any[]>([]);
|
2025-11-10 16:28:01 +08:00
|
|
|
|
2026-06-24 09:31:10 +08:00
|
|
|
// 面试邀约相关状态
|
|
|
|
|
const [interviewForm] = Form.useForm();
|
|
|
|
|
const [interviewMethod, setInterviewMethod] = useState<string>('online');
|
|
|
|
|
const [submitting, setSubmitting] = useState<boolean>(false);
|
2026-06-24 18:18:42 +08:00
|
|
|
const [alreadyInvited, setAlreadyInvited] = useState<boolean>(false);
|
|
|
|
|
const [checkingInvitation, setCheckingInvitation] = useState<boolean>(false);
|
2026-06-24 09:31:10 +08:00
|
|
|
|
2026-06-27 17:57:45 +08:00
|
|
|
// 性别映射
|
|
|
|
|
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': '民主党派',
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-10 16:28:01 +08:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (props.open && props.values) {
|
|
|
|
|
fetchResumeDetail();
|
2026-06-27 17:57:45 +08:00
|
|
|
fetchSkills();
|
2026-06-24 18:18:42 +08:00
|
|
|
checkExistingInvitation();
|
2026-06-24 09:31:10 +08:00
|
|
|
interviewForm.resetFields();
|
|
|
|
|
setInterviewMethod('online');
|
2025-11-10 16:28:01 +08:00
|
|
|
}
|
2026-06-27 17:57:45 +08:00
|
|
|
}, [props.values, props.open]);
|
2025-11-10 16:28:01 +08:00
|
|
|
|
2026-06-24 18:18:42 +08:00
|
|
|
// 检查是否已存在有效邀约
|
|
|
|
|
const checkExistingInvitation = async () => {
|
|
|
|
|
if (!props.matching?.jobId || !props.values?.userId) return;
|
|
|
|
|
setCheckingInvitation(true);
|
|
|
|
|
try {
|
|
|
|
|
const res = await getCmsInterviewList({
|
|
|
|
|
jobId: Number(props.matching.jobId),
|
|
|
|
|
userId: props.values.userId,
|
|
|
|
|
});
|
|
|
|
|
if (res?.code === 200 && res?.rows?.length > 0) {
|
|
|
|
|
const hasActive = res.rows.some(
|
|
|
|
|
(item: API.Management.InterviewInvitation) => item.status !== 'rejected'
|
|
|
|
|
);
|
|
|
|
|
setAlreadyInvited(hasActive);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('检查邀约状态失败:', error);
|
|
|
|
|
} finally {
|
|
|
|
|
setCheckingInvitation(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-10 16:28:01 +08:00
|
|
|
const fetchResumeDetail = async () => {
|
|
|
|
|
if (!props.values?.userId) return;
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
2026-06-27 17:57:45 +08:00
|
|
|
const parm = { userId: props.values.userId, jobId: props.matching?.jobId };
|
2025-11-10 16:28:01 +08:00
|
|
|
const response = await addCmsUserWorkExperiencesList(parm);
|
|
|
|
|
if (response.code === 200) {
|
2026-06-27 17:57:45 +08:00
|
|
|
setExperienceList(response.rows);
|
2025-11-10 16:28:01 +08:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取经历详情失败:', error);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-27 17:57:45 +08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-24 09:31:10 +08:00
|
|
|
// 发送面试邀约
|
|
|
|
|
const handleInterviewSubmit = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const values = await interviewForm.validateFields();
|
|
|
|
|
setSubmitting(true);
|
|
|
|
|
|
|
|
|
|
const payload: API.Management.InterviewInvitation = {
|
|
|
|
|
companyId: props.matching?.companyId,
|
|
|
|
|
jobId: Number(props.matching?.jobId),
|
|
|
|
|
userId: props.values?.userId,
|
|
|
|
|
applyId: props.values?.applyId ? Number(props.values.applyId) : undefined,
|
2026-06-24 18:18:42 +08:00
|
|
|
jobName: props.matching?.jobName,
|
2026-06-24 09:31:10 +08:00
|
|
|
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,
|
2026-06-25 13:32:03 +08:00
|
|
|
interviewerName: values.interviewerName,
|
2026-06-24 09:31:10 +08:00
|
|
|
status: 'pending',
|
|
|
|
|
remark: values.remark,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const response = await addCmsInterview(payload);
|
|
|
|
|
if (response.code === 200) {
|
|
|
|
|
message.success('面试邀约已发送');
|
2026-06-24 18:18:42 +08:00
|
|
|
setAlreadyInvited(true);
|
2026-06-24 09:31:10 +08:00
|
|
|
interviewForm.resetFields();
|
|
|
|
|
setInterviewMethod('online');
|
|
|
|
|
} else {
|
|
|
|
|
message.error(response.msg || '发送失败');
|
|
|
|
|
}
|
2026-06-24 18:18:42 +08:00
|
|
|
} catch (error: any) {
|
|
|
|
|
const errMsg = error?.response?.data?.msg || error?.message || '发送面试邀约失败';
|
|
|
|
|
message.error(errMsg);
|
2026-06-24 09:31:10 +08:00
|
|
|
console.error('发送面试邀约失败:', error);
|
|
|
|
|
} finally {
|
|
|
|
|
setSubmitting(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-27 17:57:45 +08:00
|
|
|
const values = props.values;
|
|
|
|
|
|
2025-11-10 16:28:01 +08:00
|
|
|
return (
|
|
|
|
|
<Modal
|
|
|
|
|
open={props.open}
|
|
|
|
|
onCancel={() => props.onClose()}
|
|
|
|
|
title="简历详情"
|
2026-06-27 17:57:45 +08:00
|
|
|
width={900}
|
2025-11-10 16:28:01 +08:00
|
|
|
footer={[
|
|
|
|
|
<Button key="cancel" onClick={() => props.onClose()}>
|
|
|
|
|
取消
|
2026-06-27 17:57:45 +08:00
|
|
|
</Button>,
|
2025-11-10 16:28:01 +08:00
|
|
|
]}
|
2026-06-27 17:57:45 +08:00
|
|
|
destroyOnClose
|
2025-11-10 16:28:01 +08:00
|
|
|
>
|
2026-06-27 17:57:45 +08:00
|
|
|
{!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="暂无工作经历" />
|
|
|
|
|
) : (
|
2025-11-10 16:28:01 +08:00
|
|
|
<List
|
|
|
|
|
itemLayout="vertical"
|
2026-06-27 17:57:45 +08:00
|
|
|
dataSource={experienceList}
|
|
|
|
|
renderItem={(item: any, index: number) => (
|
|
|
|
|
<List.Item style={{ marginBottom: 12 }}>
|
2025-11-10 16:28:01 +08:00
|
|
|
<Card
|
2026-06-27 17:57:45 +08:00
|
|
|
title={`经历 #${index + 1}`}
|
2025-11-10 16:28:01 +08:00
|
|
|
bordered
|
2026-06-27 17:57:45 +08:00
|
|
|
size="small"
|
|
|
|
|
style={{ width: '100%', boxShadow: '0 1px 4px rgba(0,0,0,0.08)' }}
|
2025-11-10 16:28:01 +08:00
|
|
|
>
|
|
|
|
|
<Descriptions column={2} size="small">
|
2026-06-27 17:57:45 +08:00
|
|
|
<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 || '-'}
|
2025-11-10 16:28:01 +08:00
|
|
|
</Descriptions.Item>
|
|
|
|
|
</Descriptions>
|
|
|
|
|
</Card>
|
|
|
|
|
</List.Item>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
2026-06-27 17:57:45 +08:00
|
|
|
)}
|
|
|
|
|
</Spin>
|
|
|
|
|
</Card>
|
2026-06-24 09:31:10 +08:00
|
|
|
|
2026-06-27 17:57:45 +08:00
|
|
|
{/* 面试邀约 */}
|
|
|
|
|
<Card title="面试邀约" bordered>
|
|
|
|
|
<Form
|
|
|
|
|
form={interviewForm}
|
|
|
|
|
layout="vertical"
|
|
|
|
|
initialValues={{ interviewMethod: 'online', meetingLink: 'https://mc.repohub.cn/pro.html' }}
|
2026-06-24 09:31:10 +08:00
|
|
|
>
|
|
|
|
|
<Form.Item
|
2026-06-27 17:57:45 +08:00
|
|
|
name="interviewMethod"
|
|
|
|
|
label="面试方式"
|
|
|
|
|
rules={[{ required: true, message: '请选择面试方式' }]}
|
2026-06-24 09:31:10 +08:00
|
|
|
>
|
2026-06-27 17:57:45 +08:00
|
|
|
<Select
|
|
|
|
|
placeholder="请选择面试方式"
|
|
|
|
|
onChange={(value) => setInterviewMethod(value)}
|
|
|
|
|
>
|
|
|
|
|
<Option value="online">线上</Option>
|
|
|
|
|
<Option value="offline">线下</Option>
|
|
|
|
|
</Select>
|
2026-06-24 09:31:10 +08:00
|
|
|
</Form.Item>
|
2026-06-27 17:57:45 +08:00
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
name="interviewTime"
|
|
|
|
|
label="面试时间"
|
|
|
|
|
rules={[{ required: true, message: '请选择面试时间' }]}
|
2026-06-24 09:31:10 +08:00
|
|
|
>
|
2026-06-27 17:57:45 +08:00
|
|
|
<DatePicker
|
|
|
|
|
showTime
|
|
|
|
|
style={{ width: '100%' }}
|
|
|
|
|
placeholder="请选择面试时间"
|
|
|
|
|
/>
|
|
|
|
|
</Form.Item>
|
2026-06-24 09:31:10 +08:00
|
|
|
|
2026-06-27 17:57:45 +08:00
|
|
|
{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>
|
|
|
|
|
)}
|
2026-06-24 09:31:10 +08:00
|
|
|
|
2026-06-27 17:57:45 +08:00
|
|
|
<Form.Item
|
|
|
|
|
name="remark"
|
|
|
|
|
label="备注"
|
|
|
|
|
>
|
|
|
|
|
<TextArea rows={3} placeholder="选填,如有其他注意事项可在此说明" />
|
|
|
|
|
</Form.Item>
|
2026-06-24 09:31:10 +08:00
|
|
|
|
2026-06-27 17:57:45 +08:00
|
|
|
<Form.Item
|
|
|
|
|
name="interviewerName"
|
|
|
|
|
label="面试官姓名"
|
|
|
|
|
rules={[{ required: true, message: '请输入面试官姓名' }]}
|
|
|
|
|
>
|
|
|
|
|
<Input placeholder="请输入面试官姓名" />
|
|
|
|
|
</Form.Item>
|
2026-06-25 13:32:03 +08:00
|
|
|
|
2026-06-27 17:57:45 +08:00
|
|
|
<Form.Item
|
|
|
|
|
name="contactPhone"
|
|
|
|
|
label="面试官联系方式"
|
|
|
|
|
rules={[{ required: true, message: '请输入面试官联系方式' }]}
|
|
|
|
|
>
|
|
|
|
|
<Input placeholder="请输入手机号或微信等联系方式" />
|
|
|
|
|
</Form.Item>
|
2026-06-24 09:31:10 +08:00
|
|
|
|
2026-06-27 17:57:45 +08:00
|
|
|
<Form.Item>
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
loading={submitting || checkingInvitation}
|
|
|
|
|
disabled={alreadyInvited}
|
|
|
|
|
onClick={handleInterviewSubmit}
|
|
|
|
|
>
|
|
|
|
|
{alreadyInvited ? '已邀约' : '发送面试邀约'}
|
|
|
|
|
</Button>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
</Card>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2025-11-10 16:28:01 +08:00
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-27 17:57:45 +08:00
|
|
|
export default ResumeViewModal;
|