新增面试邀约功能。
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
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:
179
src/pages/JobPortal/PersonalCenter/Interviews/index.tsx
Normal file
179
src/pages/JobPortal/PersonalCenter/Interviews/index.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Tag,
|
||||
Space,
|
||||
Typography,
|
||||
Button,
|
||||
Empty,
|
||||
message,
|
||||
Spin
|
||||
} from 'antd';
|
||||
import {
|
||||
EnvironmentOutlined,
|
||||
ArrowLeftOutlined,
|
||||
ClockCircleOutlined,
|
||||
VideoCameraOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { history } from '@umijs/max';
|
||||
import JobPortalHeader from '@/components/JobPortalHeader';
|
||||
import { getCmsInterviewList } from '@/services/Management/list';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
// 面试状态映射
|
||||
const statusMap: Record<string, { text: string; color: string }> = {
|
||||
pending: { text: '待确认', color: 'blue' },
|
||||
accepted: { text: '已接受', color: 'green' },
|
||||
rejected: { text: '已拒绝', color: 'red' },
|
||||
completed: { text: '已完成', color: 'purple' },
|
||||
};
|
||||
|
||||
// 从缓存获取用户信息
|
||||
const getUserIdFromCache = (): number | null => {
|
||||
try {
|
||||
const cached = localStorage.getItem('userInfo');
|
||||
if (cached) {
|
||||
const userInfo = JSON.parse(cached);
|
||||
return userInfo?.userId || null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('读取缓存用户信息失败:', error);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const InterviewsPage: React.FC = () => {
|
||||
const [interviews, setInterviews] = useState<API.Management.InterviewInvitation[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchInterviews();
|
||||
}, []);
|
||||
|
||||
const fetchInterviews = async () => {
|
||||
const userId = getUserIdFromCache();
|
||||
if (!userId) {
|
||||
message.warning('请先登录');
|
||||
history.push('/job-portal/personal-center');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await getCmsInterviewList({ userId });
|
||||
if (response.code === 200) {
|
||||
setInterviews(response.rows || []);
|
||||
} else {
|
||||
message.error(response.msg || '获取面试邀约列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取面试邀约列表失败:', error);
|
||||
message.error('获取面试邀约列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
history.push('/job-portal/personal-center');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="interviews-page">
|
||||
<JobPortalHeader showSearch={false} showHotJobs={false} />
|
||||
|
||||
<Spin spinning={loading}>
|
||||
<div className="page-content">
|
||||
<div className="back-button">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={handleBack}
|
||||
>
|
||||
返回个人中心
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="page-header">
|
||||
<Title level={2}>我的面试邀约</Title>
|
||||
<Text type="secondary">共 {interviews.length} 条记录</Text>
|
||||
</div>
|
||||
|
||||
{interviews.length === 0 && !loading ? (
|
||||
<Empty
|
||||
description="暂无面试邀约"
|
||||
style={{ marginTop: '100px' }}
|
||||
/>
|
||||
) : (
|
||||
<div className="job-list">
|
||||
{interviews.map((item, index) => {
|
||||
const st = statusMap[item.status || 'pending'] || statusMap.pending;
|
||||
return (
|
||||
<Card
|
||||
key={item.id || index}
|
||||
className="job-card"
|
||||
hoverable
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={18}>
|
||||
<div className="job-info">
|
||||
<Title level={4} style={{ marginBottom: 8 }}>
|
||||
<ClockCircleOutlined /> 面试时间:{item.interviewTime || '未指定'}
|
||||
</Title>
|
||||
<Space style={{ marginBottom: 8 }}>
|
||||
<Tag color={item.interviewMethod === 'online' ? 'blue' : 'orange'}>
|
||||
{item.interviewMethod === 'online' ? '线上' : '线下'}
|
||||
</Tag>
|
||||
<Tag color={st.color}>{st.text}</Tag>
|
||||
</Space>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
{item.interviewMethod === 'online' ? (
|
||||
<Space direction="vertical" size={4}>
|
||||
<Text>
|
||||
<VideoCameraOutlined /> 腾讯会议
|
||||
</Text>
|
||||
{item.meetingLink && (
|
||||
<Text type="secondary">
|
||||
链接:{item.meetingLink}
|
||||
</Text>
|
||||
)}
|
||||
{item.meetingPassword && (
|
||||
<Text type="secondary">
|
||||
密码:{item.meetingPassword}
|
||||
</Text>
|
||||
)}
|
||||
</Space>
|
||||
) : (
|
||||
<Text>
|
||||
<EnvironmentOutlined /> {item.interviewLocation || '未指定地点'}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
{item.contactPhone && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text type="secondary">面试官联系方式:{item.contactPhone}</Text>
|
||||
</div>
|
||||
)}
|
||||
{item.remark && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text type="secondary">备注:{item.remark}</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InterviewsPage;
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
SafetyCertificateOutlined,
|
||||
BellOutlined,
|
||||
FileTextOutlined,
|
||||
CameraOutlined
|
||||
CameraOutlined,
|
||||
CalendarOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { history } from '@umijs/max';
|
||||
import JobPortalHeader from '@/components/JobPortalHeader';
|
||||
@@ -179,6 +180,15 @@ const PersonalCenter: React.FC = () => {
|
||||
color: '#722ed1',
|
||||
link: '/job-portal/personal-center/showcase'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: '我的面试邀约',
|
||||
icon: <CalendarOutlined />,
|
||||
status: '',
|
||||
hasArrow: true,
|
||||
color: '#1677ff',
|
||||
link: '/job-portal/personal-center/interviews'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '在线面试',
|
||||
|
||||
@@ -298,6 +298,7 @@ function ManagementList() {
|
||||
<ResumeView
|
||||
values={currentRow}
|
||||
open={resumeVisible}
|
||||
matching={{ jobId, companyId: (jobInfo as any)?.companyId, applyId: currentRow?.applyId }}
|
||||
onClose={() => {
|
||||
setResumeVisible(false);
|
||||
setCurrentRow(undefined);
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import {Modal, Card, Descriptions, List, Button} from 'antd';
|
||||
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 {
|
||||
addCmsUserWorkExperiencesList
|
||||
addCmsUserWorkExperiencesList,
|
||||
addCmsInterview,
|
||||
} from '@/services/Management/list';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
|
||||
export type ListFormProps = {
|
||||
onClose: (flag?: boolean, formVals?: unknown) => void;
|
||||
open: boolean;
|
||||
@@ -17,10 +23,18 @@ const listEdit: React.FC<ListFormProps> = (props) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [contactInfoList, setContactInfoList] = useState<API.Management.WorkExperence[]>([]);
|
||||
|
||||
// 面试邀约相关状态
|
||||
const [interviewForm] = Form.useForm();
|
||||
const [interviewMethod, setInterviewMethod] = useState<string>('online');
|
||||
const [submitting, setSubmitting] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.open && props.values) {
|
||||
console.log(props.values);
|
||||
fetchResumeDetail();
|
||||
// 重置面试邀约表单
|
||||
interviewForm.resetFields();
|
||||
setInterviewMethod('online');
|
||||
}
|
||||
}, [props.values,props.open]);
|
||||
|
||||
@@ -41,6 +55,44 @@ const listEdit: React.FC<ListFormProps> = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 发送面试邀约
|
||||
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,
|
||||
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,
|
||||
status: 'pending',
|
||||
remark: values.remark,
|
||||
};
|
||||
|
||||
const response = await addCmsInterview(payload);
|
||||
if (response.code === 200) {
|
||||
message.success('面试邀约已发送');
|
||||
interviewForm.resetFields();
|
||||
setInterviewMethod('online');
|
||||
} else {
|
||||
message.error(response.msg || '发送失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送面试邀约失败:', error);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={props.open}
|
||||
@@ -98,6 +150,91 @@ const listEdit: React.FC<ListFormProps> = (props) => {
|
||||
/>
|
||||
}
|
||||
</ProDescriptions>
|
||||
|
||||
<Divider />
|
||||
<Card title="面试邀约" bordered style={{ marginTop: 16 }}>
|
||||
<Form
|
||||
form={interviewForm}
|
||||
layout="vertical"
|
||||
initialValues={{ interviewMethod: 'online' }}
|
||||
>
|
||||
<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="腾讯会议链接"
|
||||
rules={[{ required: true, message: '请输入腾讯会议链接' }]}
|
||||
>
|
||||
<Input placeholder="请输入腾讯会议链接" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="meetingPassword"
|
||||
label="会议密码"
|
||||
rules={[{ required: true, message: '请输入会议密码' }]}
|
||||
>
|
||||
<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="contactPhone"
|
||||
label="面试官联系方式"
|
||||
rules={[{ required: true, message: '请输入面试官联系方式' }]}
|
||||
>
|
||||
<Input placeholder="请输入手机号或微信等联系方式" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" loading={submitting} onClick={handleInterviewSubmit}>
|
||||
发送面试邀约
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -87,3 +87,38 @@ export async function addCmsUserWorkExperiencesList(params?: API.Management.Para
|
||||
params: params,
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 面试邀约 APIs ==========
|
||||
|
||||
export async function getCmsInterviewList(params?: API.Management.InterviewInvitation) {
|
||||
return request<API.Management.InterviewPageResult>(`/api/cms/interview/list`, {
|
||||
method: 'GET',
|
||||
params: params,
|
||||
});
|
||||
}
|
||||
|
||||
export async function addCmsInterview(params?: API.Management.InterviewInvitation) {
|
||||
return request<API.Management.ManagePageResult>(`/api/cms/interview`, {
|
||||
method: 'POST',
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateCmsInterview(params?: API.Management.InterviewInvitation) {
|
||||
return request<API.Management.ManagePageResult>(`/api/cms/interview`, {
|
||||
method: 'PUT',
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
||||
export async function delCmsInterview(ids: string) {
|
||||
return request<API.Management.ManagePageResult>(`/api/cms/interview/${ids}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCmsInterviewDetail(id: string) {
|
||||
return request<API.Management.InterviewPageResult>(`/api/cms/interview/${id}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
27
src/types/Management/interview.d.ts
vendored
Normal file
27
src/types/Management/interview.d.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
declare namespace API.Management {
|
||||
|
||||
export interface InterviewInvitation {
|
||||
id?: number;
|
||||
companyId?: number;
|
||||
jobId?: number;
|
||||
userId?: number;
|
||||
applyId?: number;
|
||||
interviewTime?: string;
|
||||
interviewMethod?: string;
|
||||
meetingLink?: string;
|
||||
meetingPassword?: string;
|
||||
interviewLocation?: string;
|
||||
contactPhone?: string;
|
||||
status?: string;
|
||||
remark?: string;
|
||||
createTime?: string;
|
||||
}
|
||||
|
||||
export interface InterviewPageResult {
|
||||
code: number;
|
||||
msg: string;
|
||||
total: number;
|
||||
rows: Array<InterviewInvitation>;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user