简历筛选功能开发
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
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-07-22 18:26:51 +08:00
parent 86e70a38d0
commit cd92dd4628
6 changed files with 214 additions and 18 deletions

View File

@@ -17,9 +17,9 @@ export default {
// localhost:8000/api/** -> https://preview.pro.ant.design/api/**
'/api/': {
// 要代理的地址
target: localBackendUrl,
// target: localBackendUrl,
// target: 'http://39.98.44.136:6024/api/shihezi/', // 线上环境
// target: 'http://test.xjshzly.longbiosphere.com/api/shihezi/', // 测试环境
target: 'https://test.xjshzly.longbiosphere.com/api/shihezi/', // 测试环境
// target: 'http://wykj.cdwsx.com/api',// 后端
// target: 'http://ks.zhaopinzao8dian.com/api/ks',
// 配置了这个可以从 http 代理到 https

View File

@@ -9,7 +9,9 @@ import {
Progress,
Tag,
Spin,
Divider
Divider,
Switch,
message,
} from 'antd';
import {
UserOutlined,
@@ -24,7 +26,7 @@ import { history } from '@umijs/max';
import JobPortalHeader from '@/components/JobPortalHeader';
import useAppUserModel from '@/models/jobportal/user';
import { getDictValueEnum } from '@/services/system/dict';
import { getMyTj } from '@/services/jobportal/user';
import { getMyTj, changeResumeStatus } from '@/services/jobportal/user';
import defaultAvatar from '@/assets/avatar.jpg';
import './index.less';
@@ -51,7 +53,7 @@ const calculateResumeCompleteness = (userInfo: API.AppUserInfo | null): number =
};
const PersonalCenter: React.FC = () => {
const { userInfo, loading, fetchUserInfo } = useAppUserModel();
const { userInfo, loading, fetchUserInfo, updateUserInfo } = useAppUserModel();
const [dictData, setDictData] = useState<{
sex?: Record<string, any>;
education?: Record<string, any>;
@@ -166,6 +168,25 @@ const PersonalCenter: React.FC = () => {
return !!(userInfo?.idCard && userInfo.idCard.trim());
}, [userInfo?.idCard]);
const handleResumeStatusChange = async (checked: boolean) => {
if (!userInfo?.userId) return;
const newStatus = checked ? '1' : '2';
try {
const response = await changeResumeStatus({
userId: userInfo.userId,
resumeStatus: newStatus,
});
if (response?.code === 200) {
// 同步更新 React 状态和本地缓存
const updatedInfo = { ...userInfo, resumeStatus: newStatus };
updateUserInfo(updatedInfo);
message.success(checked ? '简历已公开' : '简历已设为不公开');
}
} catch (error) {
console.error('修改简历公开状态失败:', error);
}
};
const services = useMemo(() => [
{
id: 1,
@@ -193,15 +214,6 @@ const PersonalCenter: React.FC = () => {
color: '#1677ff',
link: '/job-portal/personal-center/interviews'
},
{
id: 2,
title: '在线面试',
icon: <SafetyCertificateOutlined />,
status: '',
hasArrow: true,
color: '#1890ff',
link: 'https://mc.repohub.cn/pro.html'
},
{
id: 4,
title: '通知与提醒',
@@ -209,8 +221,19 @@ const PersonalCenter: React.FC = () => {
status: '已开启',
hasArrow: false,
color: '#fa8c16'
}
], [isRealNameVerified]);
},
{
id: 7,
title: '公开简历',
icon: <FileTextOutlined />,
hasArrow: false,
color: '#1677ff',
switchConfig: {
checked: userInfo?.resumeStatus !== '2',
onChange: handleResumeStatusChange,
}
},
], [isRealNameVerified, userInfo?.resumeStatus]);
const handleServiceClick = (service: any) => {
if (service.link) {
@@ -348,7 +371,15 @@ const PersonalCenter: React.FC = () => {
)}
</div>
</div>
{service.hasArrow && <RightOutlined className="service-arrow" />}
{service.switchConfig ? (
<Switch
checked={service.switchConfig.checked}
onChange={service.switchConfig.onChange}
onClick={(_, e) => e.stopPropagation()}
/>
) : service.hasArrow ? (
<RightOutlined className="service-arrow" />
) : null}
</List.Item>
)}
/>

View File

@@ -2,7 +2,7 @@ import React, { Fragment, useEffect, useRef, useState } from 'react';
import { FormattedMessage, useAccess, useParams } from '@umijs/max';
import { Button, FormInstance, message, Tag } from 'antd';
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
import { FormOutlined, PlusOutlined, AlignLeftOutlined, SendOutlined } from '@ant-design/icons';
import { FormOutlined, PlusOutlined, AlignLeftOutlined, SendOutlined, FilterOutlined } from '@ant-design/icons';
import { getDictValueEnum } from '@/services/system/dict';
import DictTag from '@/components/DictTag';
import { exportCmsAppUserExport } from '@/services/mobileusers/list';
@@ -16,6 +16,7 @@ import Detail from './detail';
import Hire from './hire';
import ResumeView from './resumeView';
import InterviewInvite from '../ResumeRecommend/InterviewInvite';
import ResumeFilter from './resumeFilter';
const handleExport = async (values: API.MobileUser.ListParams) => {
@@ -53,6 +54,7 @@ function ManagementList() {
const [matchingDegree, setMatchingDegree] = useState<any>(null);
const [resumeVisible, setResumeVisible] = useState<boolean>(false);
const [inviteVisible, setInviteVisible] = useState<boolean>(false);
const [filterVisible, setFilterVisible] = useState<boolean>(false);
const [hireAdd, setHireAdd] = useState<any>(null);
const [mode, setMode] = useState<'view' | 'edit' | 'create'>('create');
const params = useParams();
@@ -250,6 +252,21 @@ function ManagementList() {
>
</Button>
</div>,
<div key="second-row" style={{display: 'flex', justifyContent: 'center'}}>
<Button
type="link"
size="small"
key="resume-filter"
icon={<FilterOutlined/>}
hidden={!access.hasPerms('cms:jobApply:filterQzz')}
onClick={() => {
setCurrentRow(record);
setFilterVisible(true);
}}
>
</Button>
</div>
],
},
@@ -353,6 +370,18 @@ function ManagementList() {
}
}}
/>
<ResumeFilter
open={filterVisible}
applyId={currentRow?.applyId}
userName={currentRow?.name}
onClose={(needRefresh) => {
setFilterVisible(false);
setCurrentRow(undefined);
if (needRefresh) {
actionRef.current?.reload();
}
}}
/>
</div>
</Fragment>
);

View File

@@ -0,0 +1,119 @@
import { Modal, Form, Radio, Select, message } from 'antd';
import React, { useEffect, useState } from 'react';
import { filterQzz } from '@/services/Management/list';
import { getDictValueEnum } from '@/services/system/dict';
const { Option } = Select;
export type ResumeFilterProps = {
onClose: (needRefresh?: boolean) => void;
open: boolean;
applyId?: string;
userName?: string;
};
const ResumeFilter: React.FC<ResumeFilterProps> = (props) => {
const { open, applyId, userName } = props;
const [form] = Form.useForm();
const [submitting, setSubmitting] = useState<boolean>(false);
const [filterStatus, setFilterStatus] = useState<string>('0');
const [reasonEnum, setReasonEnum] = useState<any>({});
useEffect(() => {
if (open) {
form.resetFields();
setFilterStatus('0');
// 加载不通过筛选原因的字典
getDictValueEnum('resume_file_type', false, true).then((data) => {
setReasonEnum(data);
});
}
}, [open]);
const handleSubmit = async () => {
try {
const values = await form.validateFields();
setSubmitting(true);
const payload: { id: string; filterStatus: string; filterReason?: string } = {
id: applyId || '',
filterStatus: values.filterStatus,
};
// 如果不通过筛选,拼接原因
if (values.filterStatus === '2' && values.filterReason && values.filterReason.length > 0) {
payload.filterReason = values.filterReason.join('、');
}
const response = await filterQzz(payload);
if (response.code === 200) {
message.success('简历筛选完成');
form.resetFields();
props.onClose(true);
} else {
message.error(response.msg || '筛选失败');
}
} catch (error: any) {
const errMsg = error?.response?.data?.msg || error?.message || '简历筛选失败';
message.error(errMsg);
console.error('简历筛选失败:', error);
} finally {
setSubmitting(false);
}
};
return (
<Modal
open={open}
onCancel={() => props.onClose()}
title={`简历筛选${userName ? ` - ${userName}` : ''}`}
width={520}
onOk={handleSubmit}
confirmLoading={submitting}
destroyOnClose
>
<Form
form={form}
layout="vertical"
initialValues={{ filterStatus: '0' }}
>
<Form.Item
name="filterStatus"
label="筛选结果"
rules={[{ required: true, message: '请选择筛选结果' }]}
>
<Radio.Group
onChange={(e) => setFilterStatus(e.target.value)}
>
<Radio value="0"></Radio>
<Radio value="2"></Radio>
</Radio.Group>
</Form.Item>
{filterStatus === '2' && (
<Form.Item
name="filterReason"
label="不通过原因"
rules={[{ required: true, message: '请选择或输入不通过原因' }]}
>
<Select
mode="tags"
placeholder="请选择或输入不通过原因,按回车添加"
style={{ width: '100%' }}
tokenSeparators={['、', ',', '']}
>
{Object.keys(reasonEnum).map((key) => (
<Option key={key} value={reasonEnum[key].text}>
{reasonEnum[key].text}
</Option>
))}
</Select>
</Form.Item>
)}
</Form>
</Modal>
);
};
export default ResumeFilter;

View File

@@ -158,6 +158,14 @@ export async function getAppSkillList(params?: { userId?: number }) {
// ========== 面试消息角标 APIs ==========
/** 简历筛选 */
export async function filterQzz(params?: { id: string; filterStatus: string; filterReason?: string }) {
return request<API.Management.ManagePageResult>(`/api/cms/jobApply/filterQzz`, {
method: 'PUT',
data: params,
});
}
/** 获取面试消息角标数量(后台管理端菜单角标) */
export async function getInterviewBadgeCount() {
return request<{

View File

@@ -182,6 +182,15 @@ export async function submitJobComplaint(params: {
});
}
/** 修改简历公开状态 POST /api/cms/appUser/changeResumeStatus */
export async function changeResumeStatus(params: { userId: number; resumeStatus: string }, options?: Record<string, any>) {
return request<API.Result>('/api/cms/appUser/changeResumeStatus', {
method: 'POST',
data: params,
...(options || {}),
});
}
/** 屏蔽企业 POST /cms/blockCompany */
export async function blockCompany(params: { userId: number; companyId: number }) {
return request<API.Result>('/api/cms/blockCompany', {