Compare commits

...

7 Commits

Author SHA1 Message Date
francis-fh
767da998cb Merge branch 'FengHui'
Some checks are pending
Node CI / build (14.x, macOS-latest) (push) Waiting to run
Node CI / build (14.x, ubuntu-latest) (push) Waiting to run
Node CI / build (14.x, windows-latest) (push) Waiting to run
Node CI / build (16.x, macOS-latest) (push) Waiting to run
Node CI / build (16.x, ubuntu-latest) (push) Waiting to run
Node CI / build (16.x, windows-latest) (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
coverage CI / build (push) Waiting to run
Node pnpm CI / build (16.x, macOS-latest) (push) Waiting to run
Node pnpm CI / build (16.x, ubuntu-latest) (push) Waiting to run
Node pnpm CI / build (16.x, windows-latest) (push) Waiting to run
2026-07-24 20:58:21 +08:00
francis-fh
d653cd97d6 用户行为统计
Some checks are pending
Node CI / build (14.x, macOS-latest) (push) Waiting to run
Node CI / build (14.x, ubuntu-latest) (push) Waiting to run
Node CI / build (14.x, windows-latest) (push) Waiting to run
Node CI / build (16.x, macOS-latest) (push) Waiting to run
Node CI / build (16.x, ubuntu-latest) (push) Waiting to run
Node CI / build (16.x, windows-latest) (push) Waiting to run
coverage CI / build (push) Waiting to run
Node pnpm CI / build (16.x, macOS-latest) (push) Waiting to run
Node pnpm CI / build (16.x, ubuntu-latest) (push) Waiting to run
Node pnpm CI / build (16.x, windows-latest) (push) Waiting to run
2026-07-24 20:58:01 +08:00
francis-fh
279011e652 客户端集成简历生成器
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
2026-07-24 19:28:10 +08:00
francis-fh
0b90547ceb 11
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
2026-07-24 16:19:41 +08:00
francis-fh
970cbffe00 bug修复
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
2026-07-24 13:58:59 +08:00
francis-fh
4900040e6b 申请人简历生成器集成
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
2026-07-24 11:26:57 +08:00
francis-fh
aa48790c24 bug修复
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
2026-07-23 19:20:32 +08:00
16 changed files with 1255 additions and 1099 deletions

View File

@@ -19,8 +19,8 @@ export type MapProps = {
open: boolean;
};
export const aMapConfig = {
key: '9cfc9370bd8a941951da1cea0308e9e3',
securityJsCode: '7b16386c7f744c3ca05595965f2b037f',
key: 'b79ea14cb17704ab1a1a32c12d72f634',
securityJsCode: '04795ef5b20919d34cadc2e33e01d609',
};
/** 石河子市地图默认中心点与行政区划编码 */

View File

@@ -40,13 +40,15 @@ const ListEdit: React.FC<ListFormProps> = (props) => {
const [form] = Form.useForm();
useEffect(() => {
form.resetFields();
if (props.values) {
form.setFieldsValue({
...props.values,
});
if (props.open) {
form.resetFields();
if (props.values) {
form.setFieldsValue({
...props.values,
});
}
}
}, [form, props]);
}, [form, props.values?.companyId, props.open]);
const handleCancel = () => {
props.onCancel();
@@ -55,6 +57,7 @@ const ListEdit: React.FC<ListFormProps> = (props) => {
const handleFinish = async (values: Record<string, any>) => {
await props.onSubmit(values as API.CompanyList.Company);
return true;
};
return (

View File

@@ -146,8 +146,19 @@ function ManagementList() {
{
title: '公司行业',
dataIndex: 'industry',
valueType: 'text',
valueType: 'treeSelect',
align: 'center',
fieldProps: {
fieldNames: { label: 'label', value: 'id', children: 'children' },
treeDefaultExpandAll: false,
showSearch: true,
allowClear: true,
placeholder: '请选择行业',
},
request: async () => {
const res = await getCmsIndustryTreeList();
return (res?.data || []) as any;
},
render: (_, record) => {
return <DictTag enums={industryEnum} value={record.industry} />;
},
@@ -170,6 +181,24 @@ function ManagementList() {
valueEnum: companyNatureEnum,
render: (_, record) => <DictTag enums={companyNatureEnum} value={record.companyNature} />,
},
{
title: '法定代表人',
dataIndex: 'legalPerson',
valueType: 'text',
align: 'center',
},
{
title: '联系人',
dataIndex: 'contactPerson',
valueType: 'text',
align: 'center',
},
{
title: '联系电话',
dataIndex: 'contactPersonPhone',
valueType: 'text',
align: 'center',
},
{
title: '公司位置',
dataIndex: 'location',
@@ -329,6 +358,9 @@ function ManagementList() {
};
})
}
search={{
defaultCollapsed: false,
}}
toolBarRender={() => [
<Button
type="primary"

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useParams, history, useLocation, useSearchParams } from '@umijs/max';
import {
Card,
@@ -206,6 +206,55 @@ const JobDetailPage: React.FC = () => {
const [industryTree, setIndustryTree] = useState<any[]>([]);
const [complaintVisible, setComplaintVisible] = useState(false);
// 行为上报:浏览岗位时长统计
const enterTimeRef = useRef<number>(0);
const reportedRef = useRef<boolean>(false);
const jobIdRef = useRef<string>('');
// 页面进入/离开时上报浏览行为
useEffect(() => {
enterTimeRef.current = Date.now();
reportedRef.current = false;
jobIdRef.current = String(originalJobData?.jobId || originalJobData?.id || id || '');
const calcDuration = () =>
Math.max(1, Math.floor((Date.now() - enterTimeRef.current) / 1000));
const doReport = (durationSeconds: number) => {
if (reportedRef.current) return;
reportedRef.current = true;
const actorId = localStorage.getItem('appUserId');
const targetId = jobIdRef.current;
if (!actorId || !targetId || durationSeconds <= 0) return;
fetch('/api/cms/behavior/report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
actorType: '1',
actorId: String(actorId),
targetType: '2',
targetId: String(targetId),
viewDuration: String(durationSeconds),
}),
keepalive: true,
}).catch(() => {});
};
const handleBeforeUnload = () => {
doReport(calcDuration());
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
doReport(calcDuration());
};
}, []);
const detailTagGroups = useMemo(
() => [
{ label: '薪资范围与构成', values: jobDetail.salaryComposition },

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Card,
Row,
@@ -216,6 +216,70 @@ const JobListPage: React.FC = () => {
const [complaintVisible, setComplaintVisible] = useState(false);
const selectedJobId = selectedJob?.jobId || selectedJob?.id;
// 行为上报:浏览岗位时长统计
const jobStartTimeRef = useRef<number>(0);
const behaviorReportedRef = useRef<boolean>(false);
const currentJobIdRef = useRef<string>('');
const reportJobBehavior = useCallback((jobId: string) => {
if (behaviorReportedRef.current) return;
behaviorReportedRef.current = true;
const actorId = localStorage.getItem('appUserId');
if (!actorId || !jobId) return;
const duration = Math.max(1, Math.floor((Date.now() - jobStartTimeRef.current) / 1000));
if (duration <= 0) return;
fetch('/api/cms/behavior/report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
actorType: '1',
actorId: String(actorId),
targetType: '2',
targetId: String(jobId),
viewDuration: String(duration),
}),
keepalive: true,
}).catch(() => {});
}, []);
// 切换岗位时上报上一个岗位的浏览时长
useEffect(() => {
if (!isJobPortalLoggedIn()) return;
const newJobId = String(selectedJobId || '');
// 如果之前有浏览中的岗位,先上报
if (currentJobIdRef.current && currentJobIdRef.current !== newJobId) {
reportJobBehavior(currentJobIdRef.current);
}
// 重置计时器,开始计时新岗位
behaviorReportedRef.current = false;
jobStartTimeRef.current = Date.now();
currentJobIdRef.current = newJobId;
}, [selectedJobId, reportJobBehavior]);
// 页面离开时上报当前岗位的浏览时长
useEffect(() => {
const handleBeforeUnload = () => {
if (currentJobIdRef.current) {
reportJobBehavior(currentJobIdRef.current);
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
if (currentJobIdRef.current) {
reportJobBehavior(currentJobIdRef.current);
}
};
}, [reportJobBehavior]);
const selectedJobDetailTagGroups = useMemo(
() =>
[

View File

@@ -8,31 +8,11 @@
.jp-page-container();
padding: 20px 20px 40px;
}
}
.section {
.jp-card-base();
margin-bottom: 16px;
padding: 0;
.ant-card-head {
border-bottom: 1px solid @jp-border;
.ant-card-head-title {
color: @jp-primary;
font-weight: 600;
}
}
}
.card-profile .name {
color: @jp-text-primary;
font-weight: 600;
}
.ant-btn-primary {
background: @jp-primary;
border-color: @jp-primary;
}
// iframe 区域确保撑满
#resume-generator {
display: block;
}
@media (max-width: 1366px) {

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
import React, { Fragment, useEffect, useRef, useState } from 'react';
import { FormattedMessage, useAccess, useParams } from '@umijs/max';
import { FormattedMessage, useAccess, useNavigate, 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, FilterOutlined } from '@ant-design/icons';
import { FormOutlined, PlusOutlined, AlignLeftOutlined, SendOutlined, FilterOutlined, ArrowLeftOutlined } from '@ant-design/icons';
import { getDictValueEnum } from '@/services/system/dict';
import DictTag from '@/components/DictTag';
import { exportCmsAppUserExport } from '@/services/mobileusers/list';
@@ -14,7 +14,6 @@ import {
import similarityJobs from '@/utils/similarity_Job';
import Detail from './detail';
import Hire from './hire';
import ResumeView from './resumeView';
import InterviewInvite from '../ResumeRecommend/InterviewInvite';
import ResumeFilter from './resumeFilter';
@@ -35,6 +34,7 @@ const handleExport = async (values: API.MobileUser.ListParams) => {
function ManagementList() {
const access = useAccess();
const navigate = useNavigate();
const formTableRef = useRef<FormInstance>();
const actionRef = useRef<ActionType>();
@@ -52,7 +52,6 @@ function ManagementList() {
const [hireVisible, setHireVisible] = useState<boolean>(false);
const [jobInfo, setJobInfo] = useState({});
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);
@@ -205,11 +204,10 @@ function ManagementList() {
icon={<AlignLeftOutlined/>}
hidden={!access.hasPerms('cms:userworkexperiences:list')}
onClick={() => {
setCurrentRow(record);
setResumeVisible(true);
navigate(`/usermgmt/appuser/resume/${record.userId}?readOnly=1`);
}}
>
</Button>
<Button
type="link"
@@ -283,6 +281,7 @@ function ManagementList() {
rowKey="jobId"
key="index"
columns={columns}
search={{ defaultCollapsed: false }}
request={(params) =>
exportCmsJobCandidates(jobId).then((res) => {
// const v = similarityJobs.calculationMatchingDegreeJob(res.rows[0]);
@@ -296,6 +295,13 @@ function ManagementList() {
})
}
toolBarRender={() => [
<Button
key="back-to-list"
icon={<ArrowLeftOutlined />}
onClick={() => navigate(-1)}
>
</Button>,
<Button
type="primary"
key="export"
@@ -345,15 +351,6 @@ function ManagementList() {
}
}}
></Hire>
<ResumeView
values={currentRow}
open={resumeVisible}
matching={{ jobId, companyId: (jobInfo as any)?.companyId, applyId: currentRow?.applyId, jobName: (jobInfo as any)?.jobTitle }}
onClose={() => {
setResumeVisible(false);
setCurrentRow(undefined);
}}
/>
<InterviewInvite
open={inviteVisible}
userId={currentRow?.userId}

View File

@@ -14,7 +14,7 @@ import { useModel } from '@umijs/max';
import React, { useEffect, useRef, useState } from 'react';
import { DictValueEnumObj } from '@/components/DictTag';
import { getCmsCompanyList } from '@/services/company/list';
import { getCmsJobDetail } from '@/services/Management/list';
import { getCmsJobDetail, delCmsJobContact } from '@/services/Management/list';
import { getJobTitleTreeSelect } from '@/services/common/jobTitle';
import ProFromMap from '@/components/ProFromMap';
import { parseCommaSeparatedTags, serializeCommaSeparatedTags } from '@/utils/jobDetailTags';
@@ -32,7 +32,7 @@ const toManageFormValues = (
values: Partial<API.ManagementList.Manage>,
): Partial<ManageFormValues> => ({
...values,
jobLocationAreaCode: String(values.jobLocationAreaCode || ''),
jobLocationAreaCode: values.jobLocationAreaCode != null ? String(values.jobLocationAreaCode) : undefined,
salaryComposition: parseCommaSeparatedTags(values.salaryComposition),
welfareBenefits: parseCommaSeparatedTags(values.welfareBenefits),
workSchedule: parseCommaSeparatedTags(values.workSchedule),
@@ -65,6 +65,8 @@ export type ListFormProps = {
salaryCompositionEnum: DictValueEnumObj;
welfareBenefitsEnum: DictValueEnumObj;
workScheduleEnum: DictValueEnumObj;
keyPopulationsEnum: DictValueEnumObj;
jobFeatureEnum: DictValueEnumObj;
mode?: 'view' | 'edit' | 'create';
};
@@ -81,6 +83,8 @@ const listEdit: React.FC<ListFormProps> = (props) => {
const isEnterprise = initialState?.currentUser?.userType === 'view';
const [form] = Form.useForm<ManageFormValues>();
const companyNameMap = useRef<Record<number, string>>({});
const [companyOptions, setCompanyOptions] = useState<{ label: string; value: number }[]>([]);
const [areaOptions, setAreaOptions] = useState<{ label: string; value: string }[]>([]);
const [jobDetail, setJobDetail] = useState<API.ManagementList.Manage | null>(null);
const [loading, setLoading] = useState(false);
const [jobCategoryTreeData, setJobCategoryTreeData] = useState<any[]>([]);
@@ -94,13 +98,37 @@ const listEdit: React.FC<ListFormProps> = (props) => {
salaryCompositionEnum,
welfareBenefitsEnum,
workScheduleEnum,
keyPopulationsEnum,
jobFeatureEnum,
} = props;
const { mode = props.values ? 'edit' : 'create' } = props;
const [showKeyPopulations, setShowKeyPopulations] = useState(false);
useEffect(() => {
if (props.open) {
form.resetFields();
if (props.values) {
form.setFieldsValue(toManageFormValues(props.values));
// 预填充公司选项(编辑时回显公司名称)
if (props.values.companyId && props.values.companyName) {
companyNameMap.current[props.values.companyId] = props.values.companyName;
setCompanyOptions([
{ label: props.values.companyName, value: props.values.companyId },
]);
} else {
setCompanyOptions([]);
}
// 预填充区县选项(编辑时回显工作区县名称)
if (props.values.jobLocationAreaCode && areaEnum) {
const code = String(props.values.jobLocationAreaCode);
const areaItem = areaEnum[code];
if (areaItem) {
setAreaOptions([{ label: areaItem.label ?? areaItem.text, value: code }]);
} else {
setAreaOptions([]);
}
} else {
setAreaOptions([]);
}
// 初始化地图位置信息(编辑时回显)
if (props.values.latitude || props.values.longitude) {
setMapViewInfo({
@@ -111,8 +139,17 @@ const listEdit: React.FC<ListFormProps> = (props) => {
} else {
setMapViewInfo({});
}
// 初始化重点人群显示状态(编辑时回显)
if (props.values.isKeyPopulations === '0') {
setShowKeyPopulations(true);
} else {
setShowKeyPopulations(false);
}
} else {
setMapViewInfo({});
setShowKeyPopulations(false);
setCompanyOptions([]);
setAreaOptions([]);
}
// 加载岗位标签树数据
getJobTitleTreeSelect()
@@ -123,6 +160,22 @@ const listEdit: React.FC<ListFormProps> = (props) => {
}
}, [form, props.values?.jobId, props.open]);
// 区县字典加载完成后,回显工作区县名称
useEffect(() => {
if (
props.open &&
props.values?.jobLocationAreaCode &&
areaEnum &&
Object.keys(areaEnum).length > 0
) {
const code = String(props.values.jobLocationAreaCode);
const areaItem = areaEnum[code];
if (areaItem) {
setAreaOptions([{ label: areaItem.label ?? areaItem.text, value: code }]);
}
}
}, [areaEnum, props.open, props.values?.jobLocationAreaCode]);
// 在查看模式和编辑模式下调用API获取详情
useEffect(() => {
if (props.open && (mode === 'view' || mode === 'edit') && props.values?.jobId) {
@@ -141,6 +194,21 @@ const listEdit: React.FC<ListFormProps> = (props) => {
// 如果是编辑模式,将获取到的详情数据设置到表单中
if (mode === 'edit') {
form.setFieldsValue(toManageFormValues(response.data));
// 预填充公司选项从详情API获取的数据回显公司名称
if (response.data.companyId && response.data.companyName) {
companyNameMap.current[response.data.companyId] = response.data.companyName;
setCompanyOptions([
{ label: response.data.companyName, value: response.data.companyId },
]);
}
// 预填充区县选项从详情API获取的数据回显工作区县名称
if (response.data.jobLocationAreaCode && areaEnum) {
const code = String(response.data.jobLocationAreaCode);
const areaItem = areaEnum[code];
if (areaItem) {
setAreaOptions([{ label: areaItem.label ?? areaItem.text, value: code }]);
}
}
}
}
} catch (error) {
@@ -196,13 +264,17 @@ const listEdit: React.FC<ListFormProps> = (props) => {
description: values.description,
jobContactList: (values.jobContactList ?? []).map(
(item: API.ManagementList.ContactPerson) => ({
id: item.id,
contactPerson: item.contactPerson,
contactPersonPhone: item.contactPersonPhone,
position: item.position,
}),
),
jobCategory: values.jobCategory,
jobFeature: values.jobFeature,
isUrgent: values.isUrgent ? 1 : 0,
isKeyPopulations: showKeyPopulations ? '0' : '1',
keyPopulations: showKeyPopulations ? values.keyPopulations : undefined,
latitude: values.latitude,
longitude: values.longitude,
};
@@ -237,6 +309,16 @@ const listEdit: React.FC<ListFormProps> = (props) => {
label="是否急聘"
valueEnum={{ 0: '否', 1: '是' }}
/>
<ProDescriptions.Item
dataIndex="isKeyPopulations"
label="是否重点服务人群"
valueEnum={{ 0: '是', 1: '否' }}
/>
<ProDescriptions.Item
dataIndex="keyPopulations"
label="重点人群"
valueEnum={keyPopulationsEnum}
/>
<ProDescriptions.Item dataIndex="minSalary" label="最低薪资(元/月)" />
<ProDescriptions.Item dataIndex="maxSalary" label="最高薪资(元/月)" />
<ProDescriptions.Item
@@ -276,6 +358,11 @@ const listEdit: React.FC<ListFormProps> = (props) => {
<ProDescriptions.Item dataIndex="jobLocation" label="工作地点" />
<ProDescriptions.Item dataIndex="jobType" label="区域" valueEnum={jobTypeEnum} />
<ProDescriptions.Item dataIndex="jobCategory" label="岗位标签" />
<ProDescriptions.Item
dataIndex="jobFeature"
label="岗位特征"
valueEnum={jobFeatureEnum}
/>
<ProDescriptions.Item
dataIndex="description"
label="岗位描述"
@@ -458,6 +545,33 @@ const listEdit: React.FC<ListFormProps> = (props) => {
initialValue={0}
fieldProps={{ defaultChecked: false }}
/>
<ProFormSwitch
width="md"
name="isKeyPopulations"
label="是否重点服务人群"
checkedChildren="是"
unCheckedChildren="否"
initialValue="1"
fieldProps={{
defaultChecked: false,
onChange: (checked) => {
setShowKeyPopulations(checked);
if (!checked) {
form.setFieldValue('keyPopulations', undefined);
}
},
}}
/>
{showKeyPopulations && (
<ProFormSelect
width="md"
name="keyPopulations"
label="重点人群"
valueEnum={keyPopulationsEnum}
placeholder="请选择重点人群"
rules={[{ required: true, message: '请选择重点人群!' }]}
/>
)}
{!isEnterprise && (
<ProFormSelect
showSearch
@@ -474,6 +588,7 @@ const listEdit: React.FC<ListFormProps> = (props) => {
onChange: (companyId: number) => {
form.setFieldValue('companyName', companyNameMap.current[companyId] ?? '');
},
options: companyOptions.length > 0 ? companyOptions : undefined,
}}
placeholder="请输入公司名称选择公司"
rules={[{ required: true, message: '请输入公司名称选择公司!' }]}
@@ -543,6 +658,9 @@ const listEdit: React.FC<ListFormProps> = (props) => {
label={'工作区县'}
valueEnum={areaEnum}
placeholder="请选择区县"
fieldProps={{
options: areaOptions.length > 0 ? areaOptions : undefined,
}}
rules={[{ required: true, message: '请选择区县!' }]}
/>
<ProFormDigit
@@ -576,9 +694,16 @@ const listEdit: React.FC<ListFormProps> = (props) => {
return title.toLowerCase().includes(inputValue.toLowerCase());
}}
placeholder="请搜索或选择岗位标签"
style={{ width: '100%' }}
style={{ width: 328 }}
/>
</ProForm.Item>
<ProFormSelect
width="md"
name="jobFeature"
label="岗位特征"
valueEnum={jobFeatureEnum}
placeholder="请选择岗位特征"
/>
<ProFormText width="md" name="jobLocation" label="工作地点" placeholder="请输入工作地点" />
<ProForm.Item label="位置选择" colProps={{ span: 12 }}>
<div>
@@ -663,7 +788,13 @@ const listEdit: React.FC<ListFormProps> = (props) => {
type="text"
danger
icon={<MinusCircleOutlined />}
onClick={() => remove(name)}
onClick={async () => {
const contactId = form.getFieldValue(['jobContactList', name, 'id']);
if (contactId) {
await delCmsJobContact(String(contactId));
}
remove(name);
}}
style={{ marginBottom: 0 }}
/>
</div>

View File

@@ -97,6 +97,8 @@ function ManagementList() {
const [salaryCompositionEnum, setSalaryCompositionEnum] = useState<any>([]);
const [welfareBenefitsEnum, setWelfareBenefitsEnum] = useState<any>([]);
const [workScheduleEnum, setWorkScheduleEnum] = useState<any>([]);
const [keyPopulationsEnum, setKeyPopulationsEnum] = useState<any>([]);
const [jobFeatureEnum, setJobFeatureEnum] = useState<any>([]);
const [currentRow, setCurrentRow] = useState<API.ManagementList.Manage>();
const [modalVisible, setModalVisible] = useState<boolean>(false);
const [mode, setMode] = useState<'view' | 'edit' | 'create'>('create');
@@ -128,6 +130,12 @@ function ManagementList() {
getDictValueEnum('work_schedule', false, true).then((data) => {
setWorkScheduleEnum(data);
});
getDictValueEnum('kp_type', false, true).then((data) => {
setKeyPopulationsEnum(data);
});
getDictValueEnum('job_feature', false, true).then((data) => {
setJobFeatureEnum(data);
});
}, []);
async function changeRelease(record: API.ManagementList.Manage) {
@@ -440,6 +448,9 @@ function ManagementList() {
rowKey="jobId"
key="index"
columns={columns}
search={{
defaultCollapsed: false,
}}
request={(params) =>
getCmsJobList({ ...params } as API.ManagementList.ListParams).then((res) => {
console.log(params);
@@ -533,6 +544,8 @@ function ManagementList() {
salaryCompositionEnum={salaryCompositionEnum}
welfareBenefitsEnum={welfareBenefitsEnum}
workScheduleEnum={workScheduleEnum}
keyPopulationsEnum={keyPopulationsEnum}
jobFeatureEnum={jobFeatureEnum}
></EditManageRow>
</Fragment>
);

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useParams, useNavigate, useSearchParams } from '@umijs/max';
import { useModel, useParams, useNavigate, useSearchParams } from '@umijs/max';
import { Button, Card, message, Space, Switch, Typography } from 'antd';
import { ArrowLeftOutlined, EyeOutlined } from '@ant-design/icons';
import Iframe from 'iframe-js';
@@ -253,6 +253,60 @@ function AppUserResume() {
const iframeRef = useRef<HTMLIFrameElement>(null);
const resumeDataRef = useRef<API.CmsAppUser.ResumeData | null>(null);
// 行为上报:记录浏览时长和浏览次数
const { initialState } = useModel('@@initialState');
const enterTimeRef = useRef<number>(0);
const reportedRef = useRef<boolean>(false);
const actorIdRef = useRef<string | undefined>('');
const targetIdRef = useRef<number>(0);
actorIdRef.current = initialState?.currentUser?.userId;
targetIdRef.current = userId;
// 页面进入/离开时上报浏览行为
useEffect(() => {
enterTimeRef.current = Date.now();
reportedRef.current = false;
const doReport = (durationSeconds: number) => {
if (reportedRef.current) return;
reportedRef.current = true;
const actorId = actorIdRef.current;
const targetId = targetIdRef.current;
if (!actorId || !targetId || durationSeconds <= 0) return;
const payload = {
actorType: '2',
actorId: String(actorId),
targetType: '1',
targetId: String(targetId),
viewDuration: String(durationSeconds),
};
fetch('/api/cms/behavior/report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
keepalive: true,
}).catch(() => {});
};
const calcDuration = () =>
Math.max(1, Math.floor((Date.now() - enterTimeRef.current) / 1000));
const handleBeforeUnload = () => {
doReport(calcDuration());
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
doReport(calcDuration());
};
}, []);
// 字典数据code → 汉字)
const [sexDict, setSexDict] = useState<DictMap>({});
const [educationDict, setEducationDict] = useState<DictMap>({});

View File

@@ -1,14 +1,81 @@
import React, { useEffect, useRef, useState } from 'react';
import { Form, Input, Modal, Switch, Tag, message } from 'antd';
import { Form, Input, Modal, Switch, Tag, Button, message } from 'antd';
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
import { useAccess } from '@umijs/max';
import {
DeleteOutlined,
FormOutlined,
AlignLeftOutlined,
AuditOutlined,
PlusOutlined,
} from '@ant-design/icons';
import EditCompanyListRow, { CompanyDetailView } from '@/pages/Company/List/edit';
import {
addCmsCompanyList,
delCmsCompanyList,
getCmsCompanyDetail,
parseCmsCompanyDetailResponse,
putCmsCompanyList,
} from '@/services/company/list';
import { postCompanyApproval } from '@/services/company/review';
import { changeLcCompanyStatus, getAdminCompanyList, resetLcPsw } from '@/services/cms/company';
import { getDictValueEnum } from '@/services/system/dict';
import DictTag from '@/components/DictTag';
import { getCmsIndustryTreeList } from '@/services/classify/industry';
import DictTag, { DictValueEnumObj } from '@/components/DictTag';
/** 将行业树数据展平为 DictValueEnumObj */
const flattenIndustryTree = (tree: { id: number; label: string; children?: any[] }[]): DictValueEnumObj => {
const map: DictValueEnumObj = {};
const walk = (nodes: { id: number; label: string; children?: any[] }[]) => {
nodes?.forEach((node) => {
map[node.id] = {
text: node.label,
label: node.label,
value: node.id,
};
if (node.children?.length) {
walk(node.children);
}
});
};
walk(tree);
return map;
};
const loadCompanyDetail = async (record: API.CmsCompany.CompanyUser) => {
const resp = await getCmsCompanyDetail(record.companyId);
const detail = parseCmsCompanyDetailResponse(resp);
if (!detail) {
return { ...record } as any;
}
return { ...record, ...detail } as any;
};
function CompanyUserList() {
const access = useAccess();
const actionRef = useRef<ActionType>();
// ---- 字典数据 ----
const [scaleEnum, setScaleEnum] = useState<Record<string, any>>({});
const [natureEnum, setNatureEnum] = useState<Record<string, any>>({});
const [companyNatureEnum, setCompanyNatureEnum] = useState<DictValueEnumObj>({});
const [industryEnum, setIndustryEnum] = useState<DictValueEnumObj>({});
// ---- 编辑弹窗 ----
const [currentRow, setCurrentRow] = useState<API.CompanyList.Company>();
const [modalVisible, setModalVisible] = useState<boolean>(false);
// ---- 详情弹窗 ----
const [detailVisible, setDetailVisible] = useState<boolean>(false);
const [detailLoading, setDetailLoading] = useState<boolean>(false);
const [detailData, setDetailData] = useState<API.CompanyList.Company | undefined>();
// ---- 审核弹窗 ----
const [approvalVisible, setApprovalVisible] = useState<boolean>(false);
const [approvalStatus, setApprovalStatus] = useState<1 | 2>(1);
const [approvalReason, setApprovalReason] = useState<string>('');
// ---- 密码修改弹窗 ----
const [passwordModalOpen, setPasswordModalOpen] = useState(false);
const [currentRecord, setCurrentRecord] = useState<API.CmsCompany.CompanyUser | null>(null);
const [passwordForm] = Form.useForm();
@@ -16,8 +83,34 @@ function CompanyUserList() {
useEffect(() => {
getDictValueEnum('scale', true, true).then(setScaleEnum);
getDictValueEnum('company_nature', true, true).then(setNatureEnum);
getDictValueEnum('company_nature', false, true).then(setCompanyNatureEnum);
getCmsIndustryTreeList().then((res) => {
if (res?.data) {
setIndustryEnum(flattenIndustryTree(res.data));
}
});
}, []);
// ---- 删除处理 ----
const handleRemoveOne = async (companyId: string) => {
const hide = message.loading('正在删除');
if (!companyId) return true;
try {
const resp = await delCmsCompanyList(companyId);
hide();
if (resp.code === 200) {
message.success('删除成功,即将刷新');
} else {
message.error(resp.msg);
}
return true;
} catch (error) {
hide();
message.error('删除失败,请重试');
return false;
}
};
const columns: ProColumns<API.CmsCompany.CompanyUser>[] = [
{
title: '企业名称',
@@ -49,7 +142,7 @@ function CompanyUserList() {
align: 'center',
width: 100,
valueEnum: scaleEnum,
render: (_, record) => <DictTag enums={scaleEnum} value={record.scale} />,
render: (_, record) => <DictTag enums={scaleEnum} value={record.scale ?? undefined} />,
},
{
title: '注册地址',
@@ -68,20 +161,41 @@ function CompanyUserList() {
width: 100,
},
{
title: '联系人',
title: '企业联系人',
dataIndex: 'contactPerson',
valueType: 'text',
align: 'center',
width: 100,
render: (_, record) => record.contactPerson || '-',
width: 180,
hideInTable: true,
},
{
title: '联系电话',
title: '联系电话',
dataIndex: 'contactPersonPhone',
valueType: 'text',
align: 'center',
width: 130,
render: (_, record) => record.contactPersonPhone || '-',
width: 160,
hideInTable: true,
},
{
title: '企业联系人',
dataIndex: 'companyContactList',
valueType: 'text',
align: 'center',
width: 180,
hideInSearch: true,
render: (_, record) => {
const list = record.companyContactList;
if (!list || list.length === 0) return '-';
return (
<div>
{list.map((item, index) => (
<div key={index}>
{item.contactPerson || '-'}{item.contactPersonPhone || '-'}
</div>
))}
</div>
);
},
},
{
title: '状态',
@@ -144,11 +258,96 @@ function CompanyUserList() {
{
title: '操作',
align: 'center',
width: 120,
width: 320,
fixed: 'right',
hideInSearch: true,
render: (_, record) => (
<a
render: (_, record) => [
<Button
type="link"
size="small"
key="detail"
icon={<AlignLeftOutlined />}
hidden={!access.hasPerms('cms:company:query')}
onClick={async () => {
setDetailData({ ...record } as any);
setDetailVisible(true);
setDetailLoading(true);
try {
const merged = await loadCompanyDetail(record);
setDetailData(merged);
} catch (e) {
message.error('获取详情失败');
} finally {
setDetailLoading(false);
}
}}
>
</Button>,
<Button
type="link"
size="small"
key="edit"
icon={<FormOutlined />}
hidden={!access.hasPerms('cms:company:edit')}
onClick={async () => {
const hide = message.loading('正在加载详情');
try {
const merged = await loadCompanyDetail(record);
setCurrentRow(merged);
setModalVisible(true);
} catch (e) {
message.error('获取详情失败');
} finally {
hide();
}
}}
>
</Button>,
<Button
type="link"
size="small"
key="approval"
hidden={!access.hasPerms('sys:company:approval')}
icon={<AuditOutlined />}
onClick={() => {
setCurrentRow(record as any);
setApprovalStatus(1);
setApprovalReason('');
setApprovalVisible(true);
}}
>
</Button>,
<Button
type="link"
size="small"
danger
key="batchRemove"
icon={<DeleteOutlined />}
hidden={!access.hasPerms('cms:company:remove')}
onClick={async () => {
Modal.confirm({
title: '删除',
content: '确定删除该项吗?',
okText: '确认',
cancelText: '取消',
onOk: async () => {
const success = await handleRemoveOne(String(record.companyId));
if (success) {
actionRef.current?.reload();
}
},
});
}}
>
</Button>,
<Button
type="link"
size="small"
key="resetPwd"
onClick={() => {
setCurrentRecord(record);
passwordForm.resetFields();
@@ -156,8 +355,8 @@ function CompanyUserList() {
}}
>
</a>
),
</Button>,
],
},
];
@@ -174,11 +373,159 @@ function CompanyUserList() {
success: true,
}))
}
search={{ labelWidth: 'auto' }}
search={{ labelWidth: 'auto', defaultCollapsed: false }}
scroll={{ x: 'max-content' }}
pagination={{ defaultPageSize: 10, showSizeChanger: true }}
headerTitle="企业用户管理"
toolBarRender={() => [
<Button
type="primary"
key="add"
hidden={!access.hasPerms('cms:company:add')}
onClick={async () => {
setCurrentRow(undefined);
setModalVisible(true);
}}
>
<PlusOutlined />
</Button>,
]}
/>
{/* 编辑/新增弹窗 */}
<EditCompanyListRow
open={modalVisible}
onSubmit={async (values) => {
let resData;
if (values.companyId) {
resData = await putCmsCompanyList({
...values,
companyNature: values.companyNature || '',
});
} else {
resData = await addCmsCompanyList({
...values,
companyNature: values.companyNature || '',
});
}
if (resData.code === 200) {
setModalVisible(false);
setCurrentRow(undefined);
message.success(values.companyId ? '修改成功' : '新增成功');
actionRef.current?.reload();
}
}}
onCancel={() => {
setModalVisible(false);
setCurrentRow(undefined);
}}
values={currentRow}
scaleEnum={scaleEnum}
companyNatureEnum={companyNatureEnum}
/>
{/* 详情弹窗 */}
<CompanyDetailView
open={detailVisible}
loading={detailLoading}
onCancel={() => {
setDetailVisible(false);
setDetailData(undefined);
}}
record={detailData}
scaleEnum={scaleEnum}
companyNatureEnum={companyNatureEnum}
industryEnum={industryEnum}
/>
{/* 审核弹窗 */}
<Modal
title="企业资质审核"
open={approvalVisible}
width={520}
onCancel={() => {
setApprovalVisible(false);
setApprovalReason('');
}}
footer={null}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div>
<span style={{ marginRight: 12 }}></span>
<label style={{ marginRight: 16 }}>
<input
type="radio"
name="approvalStatus"
checked={approvalStatus === 1}
onChange={() => setApprovalStatus(1)}
/>
<span style={{ marginLeft: 6 }}></span>
</label>
<label>
<input
type="radio"
name="approvalStatus"
checked={approvalStatus === 2}
onChange={() => setApprovalStatus(2)}
/>
<span style={{ marginLeft: 6 }}></span>
</label>
</div>
{approvalStatus === 2 && (
<div>
<div style={{ marginBottom: 6 }}></div>
<textarea
style={{ width: '100%', minHeight: 100 }}
placeholder="请输入驳回原因"
value={approvalReason}
onChange={(e) => setApprovalReason(e.target.value)}
/>
</div>
)}
<div style={{ textAlign: 'right', marginTop: 8 }}>
<Button onClick={() => setApprovalVisible(false)} style={{ marginRight: 8 }}>
</Button>
<Button
type="primary"
onClick={async () => {
if (!currentRow?.companyId) {
message.error('缺少企业ID');
return;
}
if (approvalStatus === 2 && !approvalReason.trim()) {
message.warning('请填写驳回原因');
return;
}
const hide = message.loading('正在提交审核');
try {
const resp = await postCompanyApproval({
companyId: currentRow.companyId,
status: approvalStatus,
notPassReason: approvalStatus === 2 ? approvalReason.trim() : undefined,
});
hide();
if (resp?.code === 200) {
message.success('提交成功');
setApprovalVisible(false);
setApprovalReason('');
actionRef.current?.reload();
} else {
message.error(resp?.msg || '提交失败');
}
} catch (e) {
hide();
message.error('提交失败');
}
}}
>
</Button>
</div>
</div>
</Modal>
{/* 修改密码弹窗 */}
<Modal
title="修改密码"
open={passwordModalOpen}

View File

@@ -40,6 +40,12 @@ export async function delCmsJobIds(ids: string) {
});
}
export async function delCmsJobContact(ids: string) {
return request<API.ManagementList.ManagePageResult>(`/api/cms/jobcontact/${ids}`, {
method: 'DELETE',
});
}
export async function exportCmsJob(params?: API.ManagementList.ListParams) {
return downLoadXlsx(`/api/cms/job/export`, { params }, `job_data_${new Date().getTime()}.xlsx`);
}

View File

@@ -1,5 +1,6 @@
declare namespace API.ManagementList {
export interface ContactPerson {
id?: number;
contactPerson?: string;
contactPersonPhone?: string;
position?: string;
@@ -39,6 +40,10 @@ declare namespace API.ManagementList {
createTime?: string;
jobCategory?: string;
description?: string;
/** 是否重点人群 0是,1否 */
isKeyPopulations?: string;
/** 重点人群-1农民工、2团场职工、3失业人员、4零就业家庭、5零工人员、6退役军人 */
keyPopulations?: string;
}
export interface AddParams {
@@ -71,6 +76,10 @@ declare namespace API.ManagementList {
jobContactList?: ContactPerson[];
jobCategory?: string;
description?: string;
/** 是否重点人群 0是,1否 */
isKeyPopulations?: string;
/** 重点人群-1农民工、2团场职工、3失业人员、4零就业家庭、5零工人员、6退役军人 */
keyPopulations?: string;
}
export interface ListParams {

View File

@@ -7,6 +7,15 @@ declare namespace API.CmsCompany {
data?: any;
}
export interface CompanyContact {
createTime: string | null;
updateTime: string | null;
id: number | null;
companyId: number;
contactPerson: string;
contactPersonPhone: string;
}
export interface CompanyUser {
createTime: string;
updateTime: string;
@@ -29,7 +38,7 @@ declare namespace API.CmsCompany {
contactPersonPhone?: string | null;
status: number; // 0 待审核, 1 通过, 2 驳回
notPassReason?: string | null;
companyContactList?: any;
companyContactList?: CompanyContact[];
registeredAddress: string;
isAbnormal?: any;
rejectTime?: string | null;

View File

@@ -16,6 +16,7 @@ declare namespace API {
education?: string;
politicalAffiliation?: string | null;
phone?: string;
email?: string;
avatar?: string;
salaryMin?: string;
salaryMax?: string;
@@ -37,9 +38,32 @@ declare namespace API {
company?: string | null;
experiencesList?: WorkExperience[];
appSkillsList?: AppSkill[];
educationsList?: AppUserEducation[];
trainsList?: AppUserTrain[];
fileList?: any[];
}
export interface AppUserEducation {
id?: number;
userId?: number;
schoolName?: string;
major?: string;
degree?: string;
startTime?: string;
endTime?: string;
content?: string;
}
export interface AppUserTrain {
id?: number;
userId?: number;
trainOrg?: string;
trainCourse?: string;
startTime?: string;
endTime?: string;
trainContent?: string;
}
export interface WorkExperience {
createTime?: string;
updateTime?: string;