预约面试功能开发
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:
@@ -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': '' },
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -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 获取 jobId,调 API 获取数据
|
||||
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()) {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
43
src/pages/JobPortal/PersonalCenter/Interviews/index.less
Normal file
43
src/pages/JobPortal/PersonalCenter/Interviews/index.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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', {
|
||||
|
||||
3
src/types/Management/interview.d.ts
vendored
3
src/types/Management/interview.d.ts
vendored
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user