预约面试功能开发
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 18:18:42 +08:00
parent ebfb4fa580
commit 5ecce431db
14 changed files with 164 additions and 35 deletions

View File

@@ -15,8 +15,8 @@ export default {
// localhost:8000/api/** -> https://preview.pro.ant.design/api/**
'/api/': {
// 要代理的地址
target: 'http://39.98.44.136:6024/api/shihezi/',
// target: 'http://localhost:9091', // 本地代理
target: 'http://localhost:9091', // 本地开发环境
// target: 'http://39.98.44.136:6024/api/shihezi/', // 线上环境
// target: 'http://wykj.cdwsx.com/api',// 后端
// target: 'http://ks.zhaopinzao8dian.com/api/ks',
// 配置了这个可以从 http 代理到 https
@@ -26,20 +26,21 @@ export default {
// logLevel: 'debug',
},
'/profile/avatar/': {
target: 'http://36.105.163.21:30081/api/ks', // 发版
target: 'http://localhost:9091', // 本地开发环境
// target: 'http://36.105.163.21:30081/api/ks', // 发版
// target: 'http://wykj.cdwsx.com/api',
// target: 'http://ks.zhaopinzao8dian.com/api/ks',
changeOrigin: true,
},
'/app/': {
// 要代理的地址
target: 'http://36.105.163.21:30081/api/ks', // 发版
target: 'http://localhost:9091', // 本地开发环境
// target: 'http://36.105.163.21:30081/api/ks', // 发版
// target: 'http://wykj.cdwsx.com/api',
// target: 'http://ks.zhaopinzao8dian.com/api/ks',
// 配置了这个可以从 http 代理到 https
// 依赖 origin 的功能可能需要这个,比如 cookie
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
},

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useMemo } from 'react';
import { useParams, history, useLocation } from '@umijs/max';
import { useParams, history, useLocation, useSearchParams } from '@umijs/max';
import {
Card,
Typography,
@@ -32,7 +32,7 @@ import {
getEmptyCompetitivenessRadar,
} from '@/services/jobportal/competitiveness';
import { isJobPortalLoggedIn } from '@/utils/jobPortalAuth';
import { favoriteJob, unfavoriteJob, applyJob, browseJob } from '@/services/jobportal/user';
import { favoriteJob, unfavoriteJob, applyJob, browseJob, getJobDetail } from '@/services/jobportal/user';
import { ensureJobPortalLogin, requireJobPortalUserId } from '@/utils/jobPortalAuth';
import { getDictLabel, resolveIndustryLabel } from '@/utils/jobPortalDict';
import { getDictValueEnum } from '@/services/system/dict';
@@ -174,6 +174,7 @@ const mockJobDetail = {
const JobDetailPage: React.FC = () => {
const { id } = useParams<{ id: string }>();
const location = useLocation();
const [searchParams] = useSearchParams();
const [jobDetail, setJobDetail] = useState(mockJobDetail);
const [loading, setLoading] = useState(false);
const [isFavorited, setIsFavorited] = useState(false);
@@ -236,18 +237,44 @@ const JobDetailPage: React.FC = () => {
const transformedJobData = transformJobData(passedJobData);
setJobDetail(transformedJobData);
// 根据 isCollection 字段设置收藏状态
// isCollection 为 null 或 0 表示未收藏,其他值表示已收藏
setIsFavorited(passedJobData?.isCollection !== null && passedJobData?.isCollection !== 0 && passedJobData?.isCollection !== undefined);
} else {
// 如果没有传递数据则根据id从API获取数据
// fetchJobDetail(id);
// 默认未收藏
setOriginalJobData(null);
setIsFavorited(false);
// 直接刷新页面时,从 URL 参数或 route param 获取 jobIdAPI 获取数据
const jobIdFromUrl = searchParams.get('jobId');
if (jobIdFromUrl) {
fetchJobDetailFromApi(jobIdFromUrl);
} else {
setOriginalJobData(null);
setIsFavorited(false);
}
}
}, [id, location.state]);
// 页面直接刷新时调接口获取岗位详情
const fetchJobDetailFromApi = async (jobId: string) => {
try {
setLoading(true);
const res = await getJobDetail(jobId);
if (res?.code === 200 && res?.data) {
const jobData = res.data;
setOriginalJobData(jobData);
const transformed = transformJobData(jobData);
setJobDetail(transformed);
setIsFavorited(jobData?.isCollection !== null && jobData?.isCollection !== 0 && jobData?.isCollection !== undefined);
} else {
setOriginalJobData(null);
setIsFavorited(false);
}
} catch (error) {
console.error('获取岗位详情失败:', error);
setOriginalJobData(null);
setIsFavorited(false);
} finally {
setLoading(false);
}
};
// 仅登录且有真实岗位 id 时请求竞争力
useEffect(() => {
if (!isJobPortalLoggedIn()) {

View File

@@ -117,7 +117,7 @@ const MessagePage: React.FC = () => {
// 如果选择了 appointment 分类,需要前端过滤(因为接口参数中没有对应的 noticeType
if (category === 'appointment') {
messageList = messageList.filter((msg: API.Message) => {
return msg.noticeType === 'appointment' || msg.noticeType === 'interview';
return msg.noticeType === 'appointment' || msg.noticeType === 'interview' || msg.noticeType === '4';
});
}
@@ -141,7 +141,7 @@ const MessagePage: React.FC = () => {
const allMessages = response.rows || [];
setCategoryCounts({
job: allMessages.filter((m: API.Message) => m.noticeType === '2').length,
appointment: allMessages.filter((m: API.Message) => m.noticeType === 'appointment' || m.noticeType === 'interview').length,
appointment: allMessages.filter((m: API.Message) => m.noticeType === 'appointment' || m.noticeType === 'interview' || m.noticeType === '4').length,
system: allMessages.filter((m: API.Message) => m.noticeType === '1').length
});
});

View File

@@ -140,7 +140,7 @@ const ApplicationsPage: React.FC = () => {
const handleJobClick = (job: API.ApplicationJob) => {
// 跳转到岗位详情页,通过 state 传递完整的岗位数据
history.push('/job-portal/detail', { jobData: job });
history.push(`/job-portal/detail?jobId=${job.jobId || job.id}`, { jobData: job });
};
return (

View File

@@ -130,7 +130,7 @@ const FavoritesPage: React.FC = () => {
const handleJobClick = (job: API.ApplicationJob) => {
// 跳转到岗位详情页,通过 state 传递完整的岗位数据
history.push('/job-portal/detail', { jobData: job });
history.push(`/job-portal/detail?jobId=${job.jobId || job.id}`, { jobData: job });
};
const handleRemoveFavorite = async (job: API.ApplicationJob, e?: React.MouseEvent) => {

View File

@@ -128,7 +128,7 @@ const FootprintsPage: React.FC = () => {
const handleJobClick = (job: API.ApplicationJob) => {
// 跳转到岗位详情页,通过 state 传递完整的岗位数据
history.push('/job-portal/detail', { jobData: job });
history.push(`/job-portal/detail?jobId=${job.jobId || job.id}`, { jobData: job });
};
return (

View File

@@ -0,0 +1,43 @@
@import '../../theme.less';
.interviews-page {
min-height: 100vh;
background-color: @jp-bg-page;
.page-content {
.jp-page-container();
padding: 24px 20px 40px;
.back-button .ant-btn {
color: @jp-primary;
padding: 0;
&:hover {
color: @jp-primary-dark;
}
}
.page-header {
margin-bottom: 24px;
.ant-typography {
color: @jp-text-primary;
}
}
.job-list .job-card {
.jp-card-base();
margin-bottom: 16px;
transition: all 0.2s;
&:hover {
border-color: @jp-primary-border;
box-shadow: @jp-shadow-hover;
}
.job-info .ant-typography {
color: @jp-text-primary;
}
}
}
}

View File

@@ -19,7 +19,8 @@ import {
} from '@ant-design/icons';
import { history } from '@umijs/max';
import JobPortalHeader from '@/components/JobPortalHeader';
import { getCmsInterviewList } from '@/services/Management/list';
import { getAppInterviewList } from '@/services/Management/list';
import './index.less';
const { Title, Text } = Typography;
@@ -63,7 +64,7 @@ const InterviewsPage: React.FC = () => {
try {
setLoading(true);
const response = await getCmsInterviewList({ userId });
const response = await getAppInterviewList({ userId });
if (response.code === 200) {
setInterviews(response.rows || []);
} else {
@@ -120,24 +121,27 @@ const InterviewsPage: React.FC = () => {
<Row gutter={16}>
<Col span={18}>
<div className="job-info">
<Title level={4} style={{ marginBottom: 8 }}>
<Title level={4} style={{ marginBottom: 4 }}>
{item.companyName || '未知公司'} - {item.jobTitle || item.jobName || '未知岗位'}
</Title>
<Title level={5} style={{ marginBottom: 8, fontWeight: 400 }}>
<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>
{/* <Tag color={st.color}>{st.text}</Tag> */}
</Space>
<div style={{ marginTop: 8 }}>
{item.interviewMethod === 'online' ? (
<Space direction="vertical" size={4}>
<Text>
<VideoCameraOutlined />
<VideoCameraOutlined /> 线
</Text>
{item.meetingLink && (
<Text type="secondary">
{item.meetingLink}
<a href={item.meetingLink} target="_blank" rel="noopener noreferrer" style={{ color: '#1677ff' }}>{item.meetingLink}</a>
</Text>
)}
{item.meetingPassword && (

View File

@@ -241,7 +241,7 @@ const JobPortalPage: React.FC = () => {
};
const handleJobClick = (job: any) => {
history.push(`/job-portal/detail`, { jobData: job });
history.push(`/job-portal/detail?jobId=${job.jobId || job.id}`, { jobData: job });
};
const handleProfessionTagClick = (name: string) => {

View File

@@ -298,7 +298,7 @@ function ManagementList() {
<ResumeView
values={currentRow}
open={resumeVisible}
matching={{ jobId, companyId: (jobInfo as any)?.companyId, applyId: currentRow?.applyId }}
matching={{ jobId, companyId: (jobInfo as any)?.companyId, applyId: currentRow?.applyId, jobName: (jobInfo as any)?.jobTitle }}
onClose={() => {
setResumeVisible(false);
setCurrentRow(undefined);

View File

@@ -4,6 +4,7 @@ import { ProDescriptions} from '@ant-design/pro-components';
import {
addCmsUserWorkExperiencesList,
addCmsInterview,
getCmsInterviewList,
} from '@/services/Management/list';
import dayjs from 'dayjs';
@@ -27,17 +28,43 @@ const listEdit: React.FC<ListFormProps> = (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 (props.open && props.values) {
console.log(props.values);
fetchResumeDetail();
checkExistingInvitation();
// 重置面试邀约表单
interviewForm.resetFields();
setInterviewMethod('online');
}
}, [props.values,props.open]);
// 检查是否已存在有效邀约
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) {
// 存在非 rejected 的邀约即为已邀约
const hasActive = res.rows.some(
(item: API.Management.InterviewInvitation) => item.status !== 'rejected'
);
setAlreadyInvited(hasActive);
}
} catch (error) {
console.error('检查邀约状态失败:', error);
} finally {
setCheckingInvitation(false);
}
};
const fetchResumeDetail = async () => {
if (!props.values?.userId) return;
@@ -66,6 +93,7 @@ const listEdit: React.FC<ListFormProps> = (props) => {
jobId: Number(props.matching?.jobId),
userId: props.values?.userId,
applyId: props.values?.applyId ? Number(props.values.applyId) : undefined,
jobName: props.matching?.jobName,
interviewTime: values.interviewTime
? dayjs(values.interviewTime).format('YYYY-MM-DD HH:mm:ss')
: undefined,
@@ -81,12 +109,15 @@ const listEdit: React.FC<ListFormProps> = (props) => {
const response = await addCmsInterview(payload);
if (response.code === 200) {
message.success('面试邀约已发送');
setAlreadyInvited(true);
interviewForm.resetFields();
setInterviewMethod('online');
} else {
message.error(response.msg || '发送失败');
}
} catch (error) {
} catch (error: any) {
const errMsg = error?.response?.data?.msg || error?.message || '发送面试邀约失败';
message.error(errMsg);
console.error('发送面试邀约失败:', error);
} finally {
setSubmitting(false);
@@ -156,7 +187,7 @@ const listEdit: React.FC<ListFormProps> = (props) => {
<Form
form={interviewForm}
layout="vertical"
initialValues={{ interviewMethod: 'online' }}
initialValues={{ interviewMethod: 'online', meetingLink: 'https://mc.repohub.cn/pro.html' }}
>
<Form.Item
name="interviewMethod"
@@ -188,18 +219,17 @@ const listEdit: React.FC<ListFormProps> = (props) => {
<>
<Form.Item
name="meetingLink"
label="腾讯会议链接"
rules={[{ required: true, message: '请输入腾讯会议链接' }]}
label="在线会议链接"
>
<Input placeholder="请输入腾讯会议链接" />
<Input placeholder="https://mc.repohub.cn/pro.html" disabled />
</Form.Item>
<Form.Item
{/* <Form.Item
name="meetingPassword"
label="会议密码"
rules={[{ required: true, message: '请输入会议密码' }]}
>
<Input placeholder="请输入会议密码" />
</Form.Item>
</Form.Item> */}
</>
)}
@@ -229,8 +259,13 @@ const listEdit: React.FC<ListFormProps> = (props) => {
</Form.Item>
<Form.Item>
<Button type="primary" loading={submitting} onClick={handleInterviewSubmit}>
<Button
type="primary"
loading={submitting || checkingInvitation}
disabled={alreadyInvited}
onClick={handleInterviewSubmit}
>
{alreadyInvited ? '已邀约' : '发送面试邀约'}
</Button>
</Form.Item>
</Form>

View File

@@ -90,6 +90,7 @@ export async function addCmsUserWorkExperiencesList(params?: API.Management.Para
// ========== 面试邀约 APIs ==========
/** 后台管理端:面试邀约列表 */
export async function getCmsInterviewList(params?: API.Management.InterviewInvitation) {
return request<API.Management.InterviewPageResult>(`/api/cms/interview/list`, {
method: 'GET',
@@ -97,6 +98,14 @@ export async function getCmsInterviewList(params?: API.Management.InterviewInvit
});
}
/** 求职者端:我的面试邀约列表(无需后台权限) */
export async function getAppInterviewList(params?: API.Management.InterviewInvitation) {
return request<API.Management.InterviewPageResult>(`/api/app/interview/list`, {
method: 'GET',
params: params,
});
}
export async function addCmsInterview(params?: API.Management.InterviewInvitation) {
return request<API.Management.ManagePageResult>(`/api/cms/interview`, {
method: 'POST',

View File

@@ -70,6 +70,13 @@ export async function applyJob(params: { jobId: string; userId: string }, option
});
}
/** 获取岗位详情 GET /app/job/{jobId}(含 isApply / isCollection */
export async function getJobDetail(jobId: string | number) {
return request<API.Result>(`/app/job/${jobId}`, {
method: 'GET',
});
}
/** 浏览岗位 POST /api/cms/job/browse */
export async function browseJob(params: { jobId: number }, options?: Record<string, any>) {
return request<API.Result>('/api/cms/job/browse', {

View File

@@ -13,6 +13,9 @@ declare namespace API.Management {
meetingPassword?: string;
interviewLocation?: string;
contactPhone?: string;
companyName?: string;
jobName?: string;
jobTitle?: string;
status?: string;
remark?: string;
createTime?: string;