新增面试邀约功能。
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:
francis-fh
2026-06-24 09:31:10 +08:00
parent 3b1877f302
commit ebfb4fa580
7 changed files with 396 additions and 3 deletions

View 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;

View File

@@ -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: '在线面试',

View File

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

View File

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

View File

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