求职者管理增加收藏功能、面试邀请、人才库功能开发
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-16 19:34:40 +08:00
parent e4c413baad
commit 03a0f7fd29
7 changed files with 527 additions and 2 deletions

View File

@@ -0,0 +1,102 @@
import { Modal, Form, Select, Input, Button, message } from 'antd';
import React, { useEffect, useState } from 'react';
import { addHrTalentCollect } from '@/services/mobileusers/list';
const { Option } = Select;
export type CollectTalentProps = {
onClose: (needRefresh?: boolean) => void;
open: boolean;
userId?: number;
};
const CollectTalent: React.FC<CollectTalentProps> = (props) => {
const { open, userId } = props;
const [collectForm] = Form.useForm();
const [submitting, setSubmitting] = useState<boolean>(false);
useEffect(() => {
if (open) {
collectForm.resetFields();
}
}, [open]);
const handleCollectSubmit = async () => {
try {
const values = await collectForm.validateFields();
setSubmitting(true);
const response = await addHrTalentCollect({
userId: String(userId),
intentionLevel: values.intentionLevel,
collectTags: values.collectTags || undefined,
sourceType: '2', // 投递记录
});
if (response.code === 200) {
message.success('收藏成功');
collectForm.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="收藏求职者"
width={500}
footer={[
<Button key="cancel" onClick={() => props.onClose()}>
</Button>,
<Button
key="submit"
type="primary"
loading={submitting}
onClick={handleCollectSubmit}
>
</Button>,
]}
destroyOnClose
>
<Form
form={collectForm}
layout="vertical"
initialValues={{ intentionLevel: '2' }}
>
<Form.Item
name="intentionLevel"
label="意向等级"
rules={[{ required: true, message: '请选择意向等级' }]}
>
<Select placeholder="请选择意向等级">
<Option value="1"></Option>
<Option value="2"></Option>
<Option value="3"></Option>
</Select>
</Form.Item>
<Form.Item
name="collectTags"
label="人才标签"
>
<Input placeholder="请输入人才标签,如:备用人才、核心人才等" />
</Form.Item>
</Form>
</Modal>
);
};
export default CollectTalent;

View File

@@ -2,10 +2,12 @@ import React, { Fragment, useEffect, useRef, useState } from 'react';
import { FormattedMessage, useAccess } from '@umijs/max';
import {Button, FormInstance, message, Switch} from 'antd';
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
import { PlusOutlined } from '@ant-design/icons';
import { PlusOutlined, SendOutlined, StarOutlined, StarFilled } from '@ant-design/icons';
import { getDictValueEnum } from '@/services/system/dict';
import DictTag from '@/components/DictTag';
import { applyAgree, exportCmsAppUserExport, getCmsAppUserList } from '@/services/mobileusers/list';
import { applyAgree, delHrTalentCollect, exportCmsAppUserExport, getCmsAppUserList, getHrTalentCollectList } from '@/services/mobileusers/list';
import InterviewInvite from '@/pages/Management/List/ResumeRecommend/InterviewInvite';
import CollectTalent from './CollectTalent';
const handleExport = async (values: API.MobileUser.ListParams) => {
const hide = message.loading('正在导出');
@@ -35,6 +37,9 @@ function ManagementList() {
const [politicalEnum, setPoliticalEnum] = useState<any>([]);
const [currentRow, setCurrentRow] = useState<API.MobileUser.ListRow>();
const [modalVisible, setModalVisible] = useState<boolean>(false);
const [inviteVisible, setInviteVisible] = useState<boolean>(false);
const [collectVisible, setCollectVisible] = useState<boolean>(false);
const [collectMap, setCollectMap] = useState<Map<number, { id: number; intentionLevel?: string }>>(new Map());
const [isPublishEnum, setIsPublishEnum] = useState<any>([]);
useEffect(() => {
@@ -59,8 +64,46 @@ function ManagementList() {
// getDictValueEnum('job_hot',true).then((data) => {
// setHotEnum(data)
// })
fetchCollectList();
}, []);
// 获取当前企业的收藏列表,构建 userId -> collectInfo 映射
const fetchCollectList = async () => {
try {
const res: any = await getHrTalentCollectList({ pageSize: 10000 });
if (res?.code === 200 && res?.rows?.length > 0) {
const map = new Map<number, { id: number; intentionLevel?: string }>();
res.rows.forEach((item: any) => {
if (item.userId) {
map.set(item.userId, { id: item.id, intentionLevel: item.intentionLevel });
}
});
setCollectMap(map);
}
} catch (error) {
console.error('获取收藏列表失败:', error);
}
};
// 取消收藏接口路径参数为userId
const handleCancelCollect = async (record: API.MobileUser.ListRow) => {
try {
const res = await delHrTalentCollect(String(record.userId));
if (res.code === 200) {
message.success('已取消收藏');
setCollectMap((prev) => {
const next = new Map(prev);
next.delete(record.userId);
return next;
});
} else {
message.error(res.msg || '取消收藏失败');
}
} catch (error: any) {
message.error(error?.response?.data?.msg || error?.message || '取消收藏失败');
}
};
async function changeHire(record: API.MobileUser.ListRow) {
const resData = await applyAgree({
id: String(record.applyId),
@@ -180,6 +223,57 @@ function ManagementList() {
<Switch checked={record.hire === '0'} onChange={changeHire.bind(null, record)} />
),
},
{
title: '操作',
hideInSearch: true,
align: 'center',
dataIndex: 'jobId',
width: 200,
render: (_, record) => {
const isCollected = collectMap.has(record.userId);
return [
<Button
type="link"
size="small"
key="invite-interview"
icon={<SendOutlined />}
hidden={!access.hasPerms('cms:interview:add')}
onClick={() => {
setCurrentRow(record);
setInviteVisible(true);
}}
>
</Button>,
isCollected ? (
<Button
type="link"
size="small"
key="cancel-collect"
icon={<StarFilled style={{ color: '#faad14' }} />}
hidden={!access.hasPerms('cms:hrTalentCollect:remove')}
onClick={() => handleCancelCollect(record)}
>
</Button>
) : (
<Button
type="link"
size="small"
key="collect"
icon={<StarOutlined />}
hidden={!access.hasPerms('cms:hrTalentCollect:add')}
onClick={() => {
setCurrentRow(record);
setCollectVisible(true);
}}
>
</Button>
),
];
},
},
];
return (
@@ -235,6 +329,33 @@ function ManagementList() {
</Button>,
]}
/>
<InterviewInvite
open={inviteVisible}
userId={currentRow?.userId}
userName={currentRow?.name}
jobId={currentRow?.jobId}
companyId={currentRow?.companyId}
jobName={currentRow?.jobName}
applyId={currentRow?.applyId ? Number(currentRow.applyId) : undefined}
onClose={(needRefresh) => {
setInviteVisible(false);
setCurrentRow(undefined);
if (needRefresh) {
actionRef.current?.reload();
}
}}
/>
<CollectTalent
open={collectVisible}
userId={currentRow?.userId}
onClose={(needRefresh) => {
setCollectVisible(false);
setCurrentRow(undefined);
if (needRefresh) {
fetchCollectList();
}
}}
/>
</div>
</Fragment>
);

View File

@@ -0,0 +1,212 @@
import React, { Fragment, useEffect, useRef, useState } from 'react';
import { useAccess } from '@umijs/max';
import { Button, FormInstance, message, Tag } from 'antd';
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
import { StarFilled } from '@ant-design/icons';
import { getDictValueEnum } from '@/services/system/dict';
import DictTag from '@/components/DictTag';
import { delHrTalentCollect, getHrTalentCollectList } from '@/services/mobileusers/list';
function TalentCollectList() {
const access = useAccess();
const formTableRef = useRef<FormInstance>();
const actionRef = useRef<ActionType>();
const [educationEnum, setEducationEnum] = useState<any>({});
const [areaEnum, setAreaEnum] = useState<any>({});
const [sexEnum, setSexEnum] = useState<any>({});
useEffect(() => {
getDictValueEnum('education', true, true).then((data) => setEducationEnum(data));
getDictValueEnum('area', true, true).then((data) => setAreaEnum(data));
getDictValueEnum('sys_user_sex', false).then((data) => setSexEnum(data));
}, []);
// 取消收藏
const handleCancelCollect = async (record: API.TalentCollect.ListRow) => {
if (!record.userId) return;
try {
const res = await delHrTalentCollect(String(record.userId));
if (res.code === 200) {
message.success('已取消收藏');
actionRef.current?.reload();
} else {
message.error(res.msg || '取消收藏失败');
}
} catch (error: any) {
message.error(error?.response?.data?.msg || error?.message || '取消收藏失败');
}
};
const intentionLevelMap: Record<string, { text: string; color: string }> = {
'1': { text: '高意向', color: 'red' },
'2': { text: '中等意向', color: 'blue' },
'3': { text: '低意向', color: 'default' },
};
const sourceTypeMap: Record<string, string> = {
'1': '简历搜索',
'2': '投递记录',
'3': '邀约面试',
'4': '手动录入',
};
const columns: ProColumns<API.TalentCollect.ListRow>[] = [
{
title: '用户名',
dataIndex: 'name',
valueType: 'text',
align: 'center',
},
{
title: '联系方式',
dataIndex: 'phone',
valueType: 'text',
align: 'center',
},
{
title: '意向等级',
dataIndex: 'intentionLevel',
valueType: 'select',
align: 'center',
valueEnum: {
'1': '高意向',
'2': '中等意向',
'3': '低意向',
},
render: (_, record) => {
const info = intentionLevelMap[record.intentionLevel || ''];
if (!info) return '-';
return <Tag color={info.color}>{info.text}</Tag>;
},
},
{
title: '人才标签',
dataIndex: 'collectTags',
valueType: 'text',
align: 'center',
render: (_, record) => {
if (!record.collectTags) return '-';
const tags = record.collectTags.split(',').filter(Boolean);
return (
<>
{tags.map((tag) => (
<Tag key={tag} color="blue">
{tag.trim()}
</Tag>
))}
</>
);
},
},
{
title: '收藏来源',
dataIndex: 'sourceType',
valueType: 'select',
align: 'center',
valueEnum: {
'1': '简历搜索',
'2': '投递记录',
'3': '邀约面试',
'4': '手动录入',
},
render: (_, record) => sourceTypeMap[record.sourceType || ''] || '-',
},
{
title: '学历',
dataIndex: 'education',
valueType: 'select',
align: 'center',
valueEnum: educationEnum,
render: (_, record) => {
return <DictTag enums={educationEnum} value={record.education} />;
},
},
{
title: '区域',
dataIndex: 'area',
valueType: 'select',
align: 'center',
valueEnum: areaEnum,
render: (_, record) => {
return <DictTag enums={areaEnum} value={record.area} />;
},
},
{
title: '性别',
dataIndex: 'sex',
valueType: 'select',
align: 'center',
valueEnum: sexEnum,
render: (_, record) => {
return <DictTag enums={sexEnum} value={record.sex} />;
},
},
{
title: '收藏时间',
dataIndex: 'createTime',
valueType: 'dateTime',
align: 'center',
hideInSearch: true,
},
{
title: '操作',
hideInSearch: true,
align: 'center',
dataIndex: 'userId',
width: 120,
render: (_, record) => [
<Button
type="link"
size="small"
key="cancel-collect"
icon={<StarFilled style={{ color: '#faad14' }} />}
hidden={!access.hasPerms('cms:hrTalentCollect:remove')}
onClick={() => handleCancelCollect(record)}
>
</Button>,
],
},
];
return (
<Fragment>
<div style={{ width: '100%', float: 'right' }}>
<ProTable<API.TalentCollect.ListRow>
search={{ labelWidth: 'auto' }}
actionRef={actionRef}
formRef={formTableRef}
rowKey="id"
key="talentCollect"
columns={columns}
request={(params) => {
const { current, pageSize, ...searchParams } = params;
const filteredParams: Record<string, any> = {};
Object.keys(searchParams).forEach((key) => {
const val = (searchParams as any)[key];
if (val !== undefined && val !== null && val !== '') {
filteredParams[key] = val;
}
});
return getHrTalentCollectList({
pageNum: current,
pageSize,
...filteredParams,
}).then((res: any) => {
const result = {
data: res.rows,
total: res.total,
success: true,
};
return result;
});
}}
/>
</div>
</Fragment>
);
}
export default TalentCollectList;

View File

@@ -28,3 +28,33 @@ export async function applyAgree(params: { id: string; hire: number }) {
data: params,
});
}
// ========== 企业人才收藏 APIs ==========
/** 获取当前企业人才收藏列表 */
export async function getHrTalentCollectList(params?: Record<string, any>) {
return request<API.Result>('/api/cms/hrTalentCollect/list', {
method: 'GET',
params: params,
});
}
/** 收藏求职者(加入人才库) */
export async function addHrTalentCollect(data: {
userId: string;
intentionLevel: string;
collectTags?: string;
sourceType: string;
}) {
return request<API.Result>('/api/cms/hrTalentCollect', {
method: 'POST',
data: data,
});
}
/** 取消收藏移出人才库路径参数为userId */
export async function delHrTalentCollect(userId: string) {
return request<API.Result>(`/api/cms/hrTalentCollect/${userId}`, {
method: 'DELETE',
});
}

View File

@@ -9,6 +9,7 @@ declare namespace API.MobileUser {
export interface ListRow {
applyId?: string;
jobId?: string;
companyId?: number;
createTime?: string;
userId: number;
name: string;

48
src/types/talentcollect/list.d.ts vendored Normal file
View File

@@ -0,0 +1,48 @@
declare namespace API.TalentCollect {
export interface ListResult {
total: number;
rows: ListRow[];
code: number;
msg: string;
}
export interface ListRow {
id?: number;
companyId?: number;
userId?: number;
/** 意向等级 1=高意向,2=中等意向,3=低意向 */
intentionLevel?: string;
/** 意向岗位ID */
intendJobIds?: string;
/** 人才标签 */
collectTags?: string;
/** 收藏来源 1=简历搜索,2=投递记录,3=邀约面试,4=手动录入 */
sourceType?: string;
/** 备注 */
remark?: string;
createTime?: string;
// 用户信息(来自关联查询)
name?: string;
age?: string;
sex?: string;
birthDate?: string;
education?: string;
politicalAffiliation?: string;
phone?: string;
avatar?: string;
salaryMin?: string;
salaryMax?: string;
area?: string;
idCard?: string;
workExperience?: string;
}
export interface ListParams {
intentionLevel?: string;
sourceType?: string;
collectTags?: string;
name?: string;
phone?: string;
education?: string;
}
}