feat: Enhance Company and Job Portal functionality
- Added company nature selection to ManagementList and JobDetailPage. - Implemented detailed tags for job benefits and schedules in JobDetailPage and JobListPage. - Introduced CompanyInfoPage for managing company details, including nature, scale, and contacts. - Enhanced ManagementList to support salary composition, welfare benefits, and work schedule fields. - Created utility functions for parsing and serializing comma-separated tags. - Updated API services to fetch and update current company information. - Added tests for new utility functions to ensure correct parsing and serialization of tags.
This commit is contained in:
@@ -223,6 +223,11 @@ export default [
|
||||
name: 'company',
|
||||
path: '/company',
|
||||
routes: [
|
||||
{
|
||||
name: '企业信息',
|
||||
path: '/company/info/index',
|
||||
component: './Company/Info',
|
||||
},
|
||||
{
|
||||
name: '企业资质审核详情',
|
||||
path: '/company/qualification-review/detail/:id',
|
||||
|
||||
@@ -117,7 +117,7 @@ const JobComplaintModal: React.FC<JobComplaintModalProps> = ({
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
width={600}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
|
||||
174
src/pages/Company/Info/index.tsx
Normal file
174
src/pages/Company/Info/index.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Card, Form, Input, message, Select, Space, Spin, TreeSelect } from 'antd';
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
import { getCmsIndustryTreeList } from '@/services/classify/industry';
|
||||
import { getCmsCurrentCompany, putCmsCurrentCompany } from '@/services/company/list';
|
||||
import type { DictValueEnumObj } from '@/components/DictTag';
|
||||
|
||||
const CompanyInfoPage: React.FC = () => {
|
||||
const [form] = Form.useForm<API.CompanyList.Company>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [scaleEnum, setScaleEnum] = useState<DictValueEnumObj>({});
|
||||
const [companyNatureEnum, setCompanyNatureEnum] = useState<DictValueEnumObj>({});
|
||||
const [industryTree, setIndustryTree] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadCompanyInfo = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [companyRes, scaleData, companyNatureData, industryRes] = await Promise.all([
|
||||
getCmsCurrentCompany(),
|
||||
getDictValueEnum('scale', false, true),
|
||||
getDictValueEnum('company_nature', false, true),
|
||||
getCmsIndustryTreeList(),
|
||||
]);
|
||||
if (companyRes.code !== 200 || !companyRes.data) {
|
||||
message.error(companyRes.msg || '获取企业信息失败');
|
||||
return;
|
||||
}
|
||||
form.setFieldsValue(companyRes.data);
|
||||
setScaleEnum(scaleData);
|
||||
setCompanyNatureEnum(companyNatureData);
|
||||
if (industryRes?.code === 200 && industryRes.data) {
|
||||
setIndustryTree(industryRes.data);
|
||||
}
|
||||
} catch {
|
||||
message.error('获取企业信息失败,请稍后重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
void loadCompanyInfo();
|
||||
}, [form]);
|
||||
|
||||
const handleFinish = async (values: API.CompanyList.Company) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const companyContactList = (values.companyContactList || [])
|
||||
.filter((contact) => contact.contactPerson || contact.contactPersonPhone)
|
||||
.map((contact) => ({
|
||||
contactPerson: contact.contactPerson,
|
||||
contactPersonPhone: contact.contactPersonPhone,
|
||||
}));
|
||||
const response = await putCmsCurrentCompany({
|
||||
...values,
|
||||
companyNature: values.companyNature || '',
|
||||
companyContactList,
|
||||
});
|
||||
if (response.code === 200) {
|
||||
message.success('企业信息保存成功');
|
||||
form.setFieldsValue({ ...values, companyContactList });
|
||||
} else {
|
||||
message.error(response.msg || '保存失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('保存失败,请稍后重试');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="企业信息" style={{ margin: 24 }}>
|
||||
<Spin spinning={loading}>
|
||||
<Form<API.CompanyList.Company>
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleFinish}
|
||||
style={{ maxWidth: 1100 }}
|
||||
>
|
||||
<Space align="start" size={24} style={{ display: 'flex' }}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="单位名称"
|
||||
rules={[{ required: true, message: '请输入单位名称' }]}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Input placeholder="请输入单位名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="companyNature" label="企业性质" style={{ flex: 1 }}>
|
||||
<Select allowClear options={Object.values(companyNatureEnum)} placeholder="请选择企业性质" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="scale"
|
||||
label="单位规模"
|
||||
rules={[{ required: true, message: '请选择单位规模' }]}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Select options={Object.values(scaleEnum)} placeholder="请选择单位规模" />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Space align="start" size={24} style={{ display: 'flex' }}>
|
||||
<Form.Item name="code" label="信用代码" style={{ flex: 1 }}>
|
||||
<Input placeholder="请输入信用代码" />
|
||||
</Form.Item>
|
||||
<Form.Item name="industry" label="主要行业" style={{ flex: 1 }}>
|
||||
<TreeSelect
|
||||
allowClear
|
||||
showSearch
|
||||
treeData={industryTree}
|
||||
treeNodeFilterProp="label"
|
||||
fieldNames={{ label: 'label', value: 'id', children: 'children' }}
|
||||
placeholder="请选择主要行业"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="location" label="单位地点" style={{ flex: 1 }}>
|
||||
<Input placeholder="请输入单位地点" />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Form.Item name="registeredAddress" label="注册地址">
|
||||
<Input placeholder="请输入注册地址" />
|
||||
</Form.Item>
|
||||
<Space align="start" size={24} style={{ display: 'flex' }}>
|
||||
<Form.Item name="legalPerson" label="法人姓名" style={{ flex: 1 }}>
|
||||
<Input placeholder="请输入法人姓名" />
|
||||
</Form.Item>
|
||||
<Form.Item name="legalPhone" label="法人联系方式" style={{ flex: 1 }}>
|
||||
<Input placeholder="请输入法人联系方式" />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
<Form.Item name="description" label="单位介绍">
|
||||
<Input.TextArea rows={6} placeholder="请输入单位介绍" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="企业联系人">
|
||||
<Form.List name="companyContactList">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<Space key={key} align="baseline" style={{ display: 'flex', marginBottom: 8 }}>
|
||||
<Form.Item {...restField} name={[name, 'contactPerson']}>
|
||||
<Input placeholder="联系人姓名" />
|
||||
</Form.Item>
|
||||
<Form.Item {...restField} name={[name, 'contactPersonPhone']}>
|
||||
<Input placeholder="联系电话" />
|
||||
</Form.Item>
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
</Space>
|
||||
))}
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||
添加联系人
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={saving}>
|
||||
保存企业信息
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompanyInfoPage;
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
PlusOutlined,
|
||||
MinusCircleOutlined,
|
||||
DeleteOutlined,
|
||||
IdcardOutlined,
|
||||
PhoneOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
@@ -34,6 +33,7 @@ export type ListFormProps = {
|
||||
open: boolean;
|
||||
values?: Partial<API.CompanyList.Company>;
|
||||
scaleEnum?: DictValueEnumObj;
|
||||
companyNatureEnum?: DictValueEnumObj;
|
||||
};
|
||||
|
||||
const ListEdit: React.FC<ListFormProps> = (props) => {
|
||||
@@ -80,7 +80,6 @@ const ListEdit: React.FC<ListFormProps> = (props) => {
|
||||
name="name"
|
||||
label="单位名称"
|
||||
placeholder="请输入单位名称"
|
||||
disabled={!!props.values}
|
||||
/>
|
||||
<ProFormSelect
|
||||
width="md"
|
||||
@@ -89,16 +88,22 @@ const ListEdit: React.FC<ListFormProps> = (props) => {
|
||||
valueEnum={props.scaleEnum}
|
||||
placeholder="请选择单位规模"
|
||||
rules={[{ required: true, message: '请选择单位规模!' }]}
|
||||
disabled={!!props.values}
|
||||
/>
|
||||
</ProForm.Group>
|
||||
<ProFormSelect
|
||||
width="md"
|
||||
name="companyNature"
|
||||
label="企业性质"
|
||||
valueEnum={props.companyNatureEnum}
|
||||
placeholder="请选择企业性质"
|
||||
fieldProps={{ allowClear: true }}
|
||||
/>
|
||||
<ProForm.Group>
|
||||
<ProFormText
|
||||
width="md"
|
||||
name="code"
|
||||
label="信用代码"
|
||||
placeholder="请输入信用代码"
|
||||
disabled={!!props.values}
|
||||
/>
|
||||
<ProFormTreeSelect
|
||||
name="industry"
|
||||
@@ -106,7 +111,6 @@ const ListEdit: React.FC<ListFormProps> = (props) => {
|
||||
placeholder="请输入主要行业"
|
||||
allowClear
|
||||
width="md"
|
||||
disabled={!!props.values}
|
||||
request={async () => {
|
||||
return getCmsIndustryTreeList().then((res) => {
|
||||
return res.data;
|
||||
@@ -135,16 +139,34 @@ const ListEdit: React.FC<ListFormProps> = (props) => {
|
||||
name="location"
|
||||
label="单位地点"
|
||||
placeholder="请输入单位地点"
|
||||
disabled={!!props.values}
|
||||
/>
|
||||
<ProFormText
|
||||
width="xl"
|
||||
name="registeredAddress"
|
||||
label="注册地址"
|
||||
placeholder="请输入注册地址"
|
||||
/>
|
||||
</ProForm.Group>
|
||||
<ProForm.Group>
|
||||
<ProFormText
|
||||
width="md"
|
||||
name="legalPerson"
|
||||
label="法人姓名"
|
||||
placeholder="请输入法人姓名"
|
||||
/>
|
||||
<ProFormText
|
||||
width="md"
|
||||
name="legalPhone"
|
||||
label="法人联系方式"
|
||||
placeholder="请输入法人联系方式"
|
||||
/>
|
||||
</ProForm.Group>
|
||||
<ProFormTextArea
|
||||
width="xl"
|
||||
name="description"
|
||||
label="单位介绍"
|
||||
placeholder="请输入单位介绍"
|
||||
disabled={!!props.values}
|
||||
/>
|
||||
</ProForm.Group>
|
||||
|
||||
{/* 企业联系人动态表单 */}
|
||||
<ProForm.Item label="企业联系人">
|
||||
@@ -206,6 +228,7 @@ export const CompanyDetailView = ({
|
||||
onCancel,
|
||||
record,
|
||||
scaleEnum,
|
||||
companyNatureEnum,
|
||||
industryEnum,
|
||||
loading,
|
||||
}: {
|
||||
@@ -213,11 +236,12 @@ export const CompanyDetailView = ({
|
||||
onCancel: () => void;
|
||||
record?: API.CompanyList.Company;
|
||||
scaleEnum?: DictValueEnumObj;
|
||||
companyNatureEnum?: DictValueEnumObj;
|
||||
industryEnum?: DictValueEnumObj;
|
||||
loading?: boolean;
|
||||
}) => {
|
||||
const contacts = record?.companyContactList?.filter(
|
||||
(c) => c.contactPerson || c.contactPersonPhone || c.position,
|
||||
(c) => c.contactPerson || c.contactPersonPhone,
|
||||
);
|
||||
|
||||
const statusTag =
|
||||
@@ -254,6 +278,9 @@ export const CompanyDetailView = ({
|
||||
{record?.scale != null && record.scale !== '' && (
|
||||
<DictTag enums={scaleEnum} value={record.scale} />
|
||||
)}
|
||||
{record?.companyNature != null && record.companyNature !== '' && (
|
||||
<DictTag enums={companyNatureEnum} value={record.companyNature} />
|
||||
)}
|
||||
{statusTag}
|
||||
{record?.industry != null && record.industry !== '' ? (
|
||||
<DictTag enums={industryEnum} value={record.industry} />
|
||||
@@ -272,9 +299,25 @@ export const CompanyDetailView = ({
|
||||
<Descriptions.Item label="公司位置" span={2}>
|
||||
{renderDetailText(record?.location as string)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="企业性质">
|
||||
{record?.companyNature ? (
|
||||
<DictTag enums={companyNatureEnum} value={record.companyNature} />
|
||||
) : (
|
||||
renderDetailText()
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="信用代码" span={2}>
|
||||
{renderDetailText(record?.code)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="注册地址">
|
||||
{renderDetailText(record?.registeredAddress)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="法人姓名">
|
||||
{renderDetailText(record?.legalPerson)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="法人联系方式">
|
||||
{renderDetailText(record?.legalPhone)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
|
||||
@@ -307,12 +350,6 @@ export const CompanyDetailView = ({
|
||||
<PhoneOutlined />
|
||||
<span>{renderDetailText(contact.contactPersonPhone)}</span>
|
||||
</div>
|
||||
{contact.position?.trim() ? (
|
||||
<div className="company-detail__contact-row">
|
||||
<IdcardOutlined />
|
||||
<span>{contact.position}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -68,6 +68,7 @@ function ManagementList() {
|
||||
const [detailLoading, setDetailLoading] = useState<boolean>(false);
|
||||
const [detailData, setDetailData] = useState<API.CompanyList.Company | undefined>();
|
||||
const [scaleEnum, setScaleEnum] = useState<Record<string, any>>({});
|
||||
const [companyNatureEnum, setCompanyNatureEnum] = useState<DictValueEnumObj>({});
|
||||
const [industryEnum, setIndustryEnum] = useState<DictValueEnumObj>({});
|
||||
const [approvalVisible, setApprovalVisible] = useState<boolean>(false);
|
||||
const [approvalStatus, setApprovalStatus] = useState<1 | 2>(1);
|
||||
@@ -110,6 +111,9 @@ function ManagementList() {
|
||||
getDictValueEnum('scale', true, true).then((data) => {
|
||||
setScaleEnum(data);
|
||||
});
|
||||
getDictValueEnum('company_nature', false, true).then((data) => {
|
||||
setCompanyNatureEnum(data);
|
||||
});
|
||||
getCmsIndustryTreeList().then((res) => {
|
||||
if (res?.data) {
|
||||
setIndustryEnum(flattenIndustryTree(res.data));
|
||||
@@ -158,6 +162,14 @@ function ManagementList() {
|
||||
return <DictTag enums={scaleEnum} value={record.scale} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '企业性质',
|
||||
dataIndex: 'companyNature',
|
||||
valueType: 'select',
|
||||
align: 'center',
|
||||
valueEnum: companyNatureEnum,
|
||||
render: (_, record) => <DictTag enums={companyNatureEnum} value={record.companyNature} />,
|
||||
},
|
||||
{
|
||||
title: '公司位置',
|
||||
dataIndex: 'location',
|
||||
@@ -350,9 +362,15 @@ function ManagementList() {
|
||||
onSubmit={async (values) => {
|
||||
let resData;
|
||||
if (values.companyId) {
|
||||
resData = await putCmsCompanyList(values);
|
||||
resData = await putCmsCompanyList({
|
||||
...values,
|
||||
companyNature: values.companyNature || '',
|
||||
});
|
||||
} else {
|
||||
resData = await addCmsCompanyList(values);
|
||||
resData = await addCmsCompanyList({
|
||||
...values,
|
||||
companyNature: values.companyNature || '',
|
||||
});
|
||||
}
|
||||
if (resData.code === 200) {
|
||||
setModalVisible(false);
|
||||
@@ -367,6 +385,7 @@ function ManagementList() {
|
||||
}}
|
||||
values={currentRow}
|
||||
scaleEnum={scaleEnum}
|
||||
companyNatureEnum={companyNatureEnum}
|
||||
/>
|
||||
|
||||
<CompanyDetailView
|
||||
@@ -378,6 +397,7 @@ function ManagementList() {
|
||||
}}
|
||||
record={detailData}
|
||||
scaleEnum={scaleEnum}
|
||||
companyNatureEnum={companyNatureEnum}
|
||||
industryEnum={industryEnum}
|
||||
/>
|
||||
|
||||
|
||||
@@ -69,6 +69,34 @@
|
||||
border: 1px solid @jp-primary-border;
|
||||
}
|
||||
|
||||
.job-detail-tags-card {
|
||||
.job-detail-tag-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.job-detail-tag-group {
|
||||
display: grid;
|
||||
grid-template-columns: 128px minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.job-detail-tag-label {
|
||||
padding-top: 2px;
|
||||
color: @jp-text-secondary;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.ant-tag {
|
||||
margin-inline-end: 0;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
}
|
||||
|
||||
.company-info-card {
|
||||
.company-overview {
|
||||
display: flex;
|
||||
@@ -163,5 +191,10 @@
|
||||
@media (max-width: 768px) {
|
||||
.job-detail-page .job-detail-container {
|
||||
padding: 0 12px;
|
||||
|
||||
.job-detail-tags-card .job-detail-tag-group {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import JobComplaintModal from '@/components/JobComplaintModal';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
import { getCmsIndustryTreeList } from '@/services/classify/industry';
|
||||
import type { DictValueEnumObj } from '@/components/DictTag';
|
||||
import { parseCommaSeparatedTags } from '@/utils/jobDetailTags';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
|
||||
@@ -87,9 +88,13 @@ const transformJobData = (jobData: any) => {
|
||||
? formatSalary(jobData.minSalary, jobData.maxSalary)
|
||||
: (jobData.salary || '面议'),
|
||||
location: jobData.jobLocation || jobData.location,
|
||||
companyNature: jobData.companyNature || jobData.company?.companyNature || '',
|
||||
experience: jobData.experience ? experienceMap[jobData.experience] || jobData.experience : '不限',
|
||||
education: jobData.education ? educationMap[jobData.education] || jobData.education : '不限',
|
||||
tags: jobData.tags || ['五险一金', '带薪年假', '年终奖'],
|
||||
tags: Array.isArray(jobData.tags) ? jobData.tags : [],
|
||||
salaryComposition: parseCommaSeparatedTags(jobData.salaryComposition),
|
||||
welfareBenefits: parseCommaSeparatedTags(jobData.welfareBenefits),
|
||||
workSchedule: parseCommaSeparatedTags(jobData.workSchedule),
|
||||
publishTime: jobData.publishTime || new Date().toISOString().split('T')[0],
|
||||
description: jobData.description || `
|
||||
<h3>职位职责:</h3>
|
||||
@@ -137,7 +142,11 @@ const mockJobDetail = {
|
||||
location: '青岛·李沧区',
|
||||
experience: '3-5年',
|
||||
education: '本科',
|
||||
tags: ['五险一金', '带薪年假', '年终奖', '定期体检'],
|
||||
companyNature: '',
|
||||
tags: [],
|
||||
salaryComposition: [] as string[],
|
||||
welfareBenefits: [] as string[],
|
||||
workSchedule: [] as string[],
|
||||
publishTime: '2024-03-15',
|
||||
description: `
|
||||
<h3>职位职责:</h3>
|
||||
@@ -193,15 +202,27 @@ const JobDetailPage: React.FC = () => {
|
||||
{ item: '工作地', score: 0 }
|
||||
]);
|
||||
const [scaleEnum, setScaleEnum] = useState<DictValueEnumObj>({});
|
||||
const [companyNatureEnum, setCompanyNatureEnum] = useState<DictValueEnumObj>({});
|
||||
const [industryTree, setIndustryTree] = useState<any[]>([]);
|
||||
const [complaintVisible, setComplaintVisible] = useState(false);
|
||||
|
||||
const detailTagGroups = useMemo(
|
||||
() => [
|
||||
{ label: '薪资范围与构成', values: jobDetail.salaryComposition },
|
||||
{ label: '福利待遇', values: jobDetail.welfareBenefits },
|
||||
{ label: '工作时间安排', values: jobDetail.workSchedule },
|
||||
].filter((group) => group.values.length > 0),
|
||||
[jobDetail.salaryComposition, jobDetail.welfareBenefits, jobDetail.workSchedule],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
getDictValueEnum('scale', true, true),
|
||||
getDictValueEnum('company_nature', false, true),
|
||||
getCmsIndustryTreeList(),
|
||||
]).then(([scaleData, industryRes]) => {
|
||||
]).then(([scaleData, companyNatureData, industryRes]) => {
|
||||
setScaleEnum(scaleData);
|
||||
setCompanyNatureEnum(companyNatureData);
|
||||
if (industryRes?.code === 200 && industryRes?.data) {
|
||||
setIndustryTree(industryRes.data);
|
||||
}
|
||||
@@ -243,6 +264,11 @@ const JobDetailPage: React.FC = () => {
|
||||
setJobDetail(transformedJobData);
|
||||
// 根据 isCollection 字段设置收藏状态
|
||||
setIsFavorited(passedJobData?.isCollection !== null && passedJobData?.isCollection !== 0 && passedJobData?.isCollection !== undefined);
|
||||
// 列表卡片不携带详情标签;进入详情页后始终请求详情接口补齐完整岗位数据。
|
||||
const passedJobId = passedJobData.jobId || passedJobData.id || id;
|
||||
if (passedJobId) {
|
||||
fetchJobDetailFromApi(String(passedJobId));
|
||||
}
|
||||
} else {
|
||||
// 直接刷新页面时,从 URL 参数或 route param 获取 jobId,调 API 获取数据
|
||||
const jobIdFromUrl = searchParams.get('jobId');
|
||||
@@ -507,6 +533,11 @@ const JobDetailPage: React.FC = () => {
|
||||
</Text>
|
||||
</Space>
|
||||
<Space wrap>
|
||||
{jobDetail.companyNature && (
|
||||
<Tag color="blue">
|
||||
{getDictLabel(companyNatureEnum, jobDetail.companyNature)}
|
||||
</Tag>
|
||||
)}
|
||||
{jobDetail.tags.map((tag, index) => (
|
||||
<Tag key={index} color="blue">{tag}</Tag>
|
||||
))}
|
||||
@@ -549,6 +580,23 @@ const JobDetailPage: React.FC = () => {
|
||||
<Row gutter={16}>
|
||||
{/* 左侧内容区 */}
|
||||
<Col span={16}>
|
||||
{detailTagGroups.length > 0 && (
|
||||
<Card title="岗位待遇与工作安排" className="info-card job-detail-tags-card">
|
||||
<div className="job-detail-tag-groups">
|
||||
{detailTagGroups.map((group) => (
|
||||
<div className="job-detail-tag-group" key={group.label}>
|
||||
<div className="job-detail-tag-label">{group.label}</div>
|
||||
<Space wrap size={[8, 8]}>
|
||||
{group.values.map((value) => (
|
||||
<Tag color="blue" key={`${group.label}-${value}`}>{value}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 职位描述 */}
|
||||
<Card title="职位描述" className="info-card">
|
||||
<div
|
||||
@@ -697,4 +745,3 @@ const JobDetailPage: React.FC = () => {
|
||||
};
|
||||
|
||||
export default JobDetailPage;
|
||||
|
||||
|
||||
@@ -209,6 +209,43 @@
|
||||
margin: 20px 0 24px;
|
||||
}
|
||||
|
||||
.job-detail-tag-section {
|
||||
margin-bottom: 28px;
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: @jp-text-primary;
|
||||
}
|
||||
|
||||
.job-detail-tag-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.job-detail-tag-group {
|
||||
display: grid;
|
||||
grid-template-columns: 128px minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.job-detail-tag-label {
|
||||
color: @jp-text-secondary;
|
||||
font-weight: 500;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.detail-tag {
|
||||
.jp-job-tag();
|
||||
margin: 0;
|
||||
padding: 2px 10px;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.job-description {
|
||||
.section-title {
|
||||
margin: 0 0 16px;
|
||||
|
||||
@@ -28,10 +28,18 @@ import {
|
||||
import { history, useLocation } from '@umijs/max';
|
||||
import { getJobList, getJobRecommend } from '@/services/common/jobTitle';
|
||||
import JobPortalHeader from '@/components/JobPortalHeader';
|
||||
import { favoriteJob, unfavoriteJob, applyJob, browseJob, blockCompany } from '@/services/jobportal/user';
|
||||
import {
|
||||
favoriteJob,
|
||||
unfavoriteJob,
|
||||
applyJob,
|
||||
browseJob,
|
||||
blockCompany,
|
||||
getJobDetail,
|
||||
} from '@/services/jobportal/user';
|
||||
import { ensureJobPortalLogin, requireJobPortalUserId } from '@/utils/jobPortalAuth';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
import { getDictLabel, findTreeLabelById, resolveIndustryLabel } from '@/utils/jobPortalDict';
|
||||
import { parseCommaSeparatedTags } from '@/utils/jobDetailTags';
|
||||
import JobComplaintModal from '@/components/JobComplaintModal';
|
||||
import { getJobTitleTreeSelect } from '@/services/common/jobTitle';
|
||||
import { getCmsIndustryTreeList } from '@/services/classify/industry';
|
||||
@@ -203,6 +211,7 @@ const JobListPage: React.FC = () => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [jobCategory, setJobCategory] = useState<string>('');
|
||||
const [scaleEnum, setScaleEnum] = useState<DictValueEnumObj>({});
|
||||
const [companyNatureEnum, setCompanyNatureEnum] = useState<DictValueEnumObj>({});
|
||||
const [jobTitleTree, setJobTitleTree] = useState<any[]>([]);
|
||||
const [industryTree, setIndustryTree] = useState<any[]>([]);
|
||||
const [searchValue, setSearchValue] = useState<string>(''); // 搜索框的值
|
||||
@@ -217,6 +226,30 @@ const JobListPage: React.FC = () => {
|
||||
{ item: '工作地', score: 0 }
|
||||
]);
|
||||
const [complaintVisible, setComplaintVisible] = useState(false);
|
||||
const selectedJobId = selectedJob?.jobId || selectedJob?.id;
|
||||
|
||||
const selectedJobDetailTagGroups = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: '薪资范围与构成',
|
||||
values: parseCommaSeparatedTags(selectedJob?.salaryComposition),
|
||||
},
|
||||
{
|
||||
label: '福利待遇',
|
||||
values: parseCommaSeparatedTags(selectedJob?.welfareBenefits),
|
||||
},
|
||||
{
|
||||
label: '工作时间安排',
|
||||
values: parseCommaSeparatedTags(selectedJob?.workSchedule),
|
||||
},
|
||||
].filter((group) => group.values.length > 0),
|
||||
[
|
||||
selectedJob?.salaryComposition,
|
||||
selectedJob?.welfareBenefits,
|
||||
selectedJob?.workSchedule,
|
||||
],
|
||||
);
|
||||
|
||||
// 格式化薪资显示
|
||||
const formatSalary = (minSalary: number, maxSalary: number) => {
|
||||
if (minSalary && maxSalary) {
|
||||
@@ -232,10 +265,12 @@ const JobListPage: React.FC = () => {
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
getDictValueEnum('scale', true, true),
|
||||
getDictValueEnum('company_nature', false, true),
|
||||
getCmsIndustryTreeList(),
|
||||
getJobTitleTreeSelect(),
|
||||
]).then(([scaleData, industryRes, jobTitleRes]) => {
|
||||
]).then(([scaleData, companyNatureData, industryRes, jobTitleRes]) => {
|
||||
setScaleEnum(scaleData);
|
||||
setCompanyNatureEnum(companyNatureData);
|
||||
if (industryRes?.code === 200 && industryRes?.data) {
|
||||
setIndustryTree(industryRes.data);
|
||||
}
|
||||
@@ -302,6 +337,32 @@ const JobListPage: React.FC = () => {
|
||||
}
|
||||
}, [jobList, selectedJob]);
|
||||
|
||||
// 列表数据不包含岗位待遇字段,选中岗位后补取完整详情。
|
||||
useEffect(() => {
|
||||
if (!selectedJobId) return;
|
||||
|
||||
let cancelled = false;
|
||||
const requestedJobId = selectedJobId;
|
||||
|
||||
getJobDetail(requestedJobId)
|
||||
.then((response) => {
|
||||
if (cancelled || response?.code !== 200 || !response?.data) return;
|
||||
|
||||
setSelectedJob((current: any) => {
|
||||
const currentJobId = current?.jobId || current?.id;
|
||||
if (String(currentJobId) !== String(requestedJobId)) return current;
|
||||
return { ...current, ...response.data };
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 详情请求失败时保留列表已有信息,不影响岗位浏览。
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [selectedJobId]);
|
||||
|
||||
// 当 selectedJob 变化时,更新收藏状态
|
||||
useEffect(() => {
|
||||
if (selectedJob) {
|
||||
@@ -313,31 +374,31 @@ const JobListPage: React.FC = () => {
|
||||
|
||||
// 当 selectedJob 变化时,获取竞争力分析数据(仅登录)
|
||||
useEffect(() => {
|
||||
if (!selectedJob) return;
|
||||
if (!selectedJobId) return;
|
||||
if (!isJobPortalLoggedIn()) {
|
||||
setOverallScore(0);
|
||||
setRadarData(getEmptyCompetitivenessRadar());
|
||||
return;
|
||||
}
|
||||
const jobIdNum = Number(selectedJob?.jobId || selectedJob?.id);
|
||||
const jobIdNum = Number(selectedJobId);
|
||||
if (!Number.isNaN(jobIdNum) && jobIdNum) {
|
||||
fetchCompetitiveness(jobIdNum);
|
||||
}
|
||||
}, [selectedJob]);
|
||||
}, [selectedJobId]);
|
||||
|
||||
// 当 selectedJob 变化时,上报浏览记录(仅登录)
|
||||
useEffect(() => {
|
||||
if (!selectedJob) return;
|
||||
if (!selectedJobId) return;
|
||||
if (!isJobPortalLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
const jobIdNum = Number(selectedJob?.jobId || selectedJob?.id);
|
||||
const jobIdNum = Number(selectedJobId);
|
||||
if (!Number.isNaN(jobIdNum) && jobIdNum > 0) {
|
||||
browseJob({ jobId: jobIdNum }).catch(() => {
|
||||
// 浏览上报失败不阻塞页面
|
||||
});
|
||||
}
|
||||
}, [selectedJob]);
|
||||
}, [selectedJobId]);
|
||||
|
||||
const fetchCompetitiveness = async (jobIdNum: number) => {
|
||||
if (!isJobPortalLoggedIn()) {
|
||||
@@ -781,6 +842,11 @@ const JobListPage: React.FC = () => {
|
||||
{job.jobCategory && (
|
||||
<Tag className="job-tag">{resolveJobCategoryLabel(job)}</Tag>
|
||||
)}
|
||||
{(job.companyNature || job.company?.companyNature) && (
|
||||
<Tag className="job-tag">
|
||||
{getDictLabel(companyNatureEnum, job.companyNature || job.company?.companyNature)}
|
||||
</Tag>
|
||||
)}
|
||||
{/* {job.dataSource && <Tag className="job-tag">{job.dataSource}</Tag>} */}
|
||||
{job.vacancies && <Tag className="job-tag">招聘{job.vacancies}人</Tag>}
|
||||
{/* {job.industry && <Tag className="job-tag">{job.industry}</Tag>} */}
|
||||
@@ -847,6 +913,14 @@ const JobListPage: React.FC = () => {
|
||||
{selectedJob?.jobCategory && (
|
||||
<Tag className="meta-tag">{resolveJobCategoryLabel(selectedJob)}</Tag>
|
||||
)}
|
||||
{(selectedJob?.companyNature || selectedJob?.company?.companyNature) && (
|
||||
<Tag className="meta-tag">
|
||||
{getDictLabel(
|
||||
companyNatureEnum,
|
||||
selectedJob.companyNature || selectedJob.company?.companyNature,
|
||||
)}
|
||||
</Tag>
|
||||
)}
|
||||
{selectedJob?.vacancies && (
|
||||
<Tag className="meta-tag">招聘{selectedJob.vacancies}人</Tag>
|
||||
)}
|
||||
@@ -891,6 +965,30 @@ const JobListPage: React.FC = () => {
|
||||
<Row gutter={16}>
|
||||
{/* 左侧内容区 */}
|
||||
<Col span={16}>
|
||||
{selectedJobDetailTagGroups.length > 0 && (
|
||||
<div className="job-detail-tag-section">
|
||||
<Title level={4} className="section-title">
|
||||
岗位待遇与工作安排
|
||||
</Title>
|
||||
<div className="job-detail-tag-groups">
|
||||
{selectedJobDetailTagGroups.map((group) => (
|
||||
<div className="job-detail-tag-group" key={group.label}>
|
||||
<div className="job-detail-tag-label">{group.label}</div>
|
||||
<Space wrap size={[8, 8]}>
|
||||
{group.values.map((value) => (
|
||||
<Tag
|
||||
className="detail-tag"
|
||||
key={`${group.label}-${value}`}
|
||||
>
|
||||
{value}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="job-description">
|
||||
<Title level={4} className="section-title">职位描述</Title>
|
||||
<Paragraph className="description-text">
|
||||
@@ -1049,4 +1147,3 @@ const JobListPage: React.FC = () => {
|
||||
};
|
||||
|
||||
export default JobListPage;
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ import { getGetInfoCache, saveGetInfoCache } from '@/utils/jobPortalAuth';
|
||||
import { getAccessToken } from '@/access';
|
||||
import { getUserInfo } from '@/services/session';
|
||||
import JobPortalHeader from '@/components/JobPortalHeader';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
import { getDictLabel } from '@/utils/jobPortalDict';
|
||||
import type { DictValueEnumObj } from '@/components/DictTag';
|
||||
import './index.less';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
@@ -128,6 +131,7 @@ const JobPortalPage: React.FC = () => {
|
||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||
const [jobTitleData, setJobTitleData] = useState<any>(null);
|
||||
const [jobRecommendData, setJobRecommendData] = useState<any>(null);
|
||||
const [companyNatureEnum, setCompanyNatureEnum] = useState<DictValueEnumObj>({});
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [itemsPerPage] = useState<number>(6);
|
||||
const [totalPages, setTotalPages] = useState<number>(1);
|
||||
@@ -189,6 +193,9 @@ const JobPortalPage: React.FC = () => {
|
||||
|
||||
fetchJobTitleData();
|
||||
fetchJobRecommendData();
|
||||
getDictValueEnum('company_nature', false, true)
|
||||
.then(setCompanyNatureEnum)
|
||||
.catch(() => setCompanyNatureEnum({}));
|
||||
}, [itemsPerPage]);
|
||||
|
||||
const handleIndustryHover = (industryId: string) => {
|
||||
@@ -290,6 +297,11 @@ const JobPortalPage: React.FC = () => {
|
||||
</div>
|
||||
<div className="job-tags">
|
||||
{job.jobCategory && <Tag className="job-tag">{job.jobCategory}</Tag>}
|
||||
{(job.companyNature || job.company?.companyNature) && (
|
||||
<Tag className="job-tag">
|
||||
{getDictLabel(companyNatureEnum, job.companyNature || job.company?.companyNature)}
|
||||
</Tag>
|
||||
)}
|
||||
{job.vacancies && <Tag className="job-tag">招聘{job.vacancies}人</Tag>}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ProFormTextArea,
|
||||
ProDescriptions,
|
||||
} from '@ant-design/pro-components';
|
||||
import { Form, Button, Input, TreeSelect, Switch } from 'antd';
|
||||
import { Form, Button, Input, Tag, TreeSelect, Switch } from 'antd';
|
||||
import { PlusOutlined, MinusCircleOutlined, EnvironmentOutlined } from '@ant-design/icons';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { DictValueEnumObj } from '@/components/DictTag';
|
||||
@@ -16,6 +16,37 @@ import { getCmsCompanyList } from '@/services/company/list';
|
||||
import { getCmsJobDetail } from '@/services/Management/list';
|
||||
import { getJobTitleTreeSelect } from '@/services/common/jobTitle';
|
||||
import ProFromMap from '@/components/ProFromMap';
|
||||
import {
|
||||
parseCommaSeparatedTags,
|
||||
serializeCommaSeparatedTags,
|
||||
} from '@/utils/jobDetailTags';
|
||||
|
||||
type ManageFormValues = Omit<
|
||||
API.ManagementList.Manage,
|
||||
'salaryComposition' | 'welfareBenefits' | 'workSchedule'
|
||||
> & {
|
||||
salaryComposition?: string[];
|
||||
welfareBenefits?: string[];
|
||||
workSchedule?: string[];
|
||||
};
|
||||
|
||||
const toManageFormValues = (
|
||||
values: Partial<API.ManagementList.Manage>,
|
||||
): Partial<ManageFormValues> => ({
|
||||
...values,
|
||||
jobLocationAreaCode: String(values.jobLocationAreaCode || ''),
|
||||
salaryComposition: parseCommaSeparatedTags(values.salaryComposition),
|
||||
welfareBenefits: parseCommaSeparatedTags(values.welfareBenefits),
|
||||
workSchedule: parseCommaSeparatedTags(values.workSchedule),
|
||||
});
|
||||
|
||||
const renderDetailTags = (value: unknown) => {
|
||||
const tags = parseCommaSeparatedTags(value);
|
||||
if (tags.length === 0) {
|
||||
return <span style={{ color: '#999' }}>暂无</span>;
|
||||
}
|
||||
return tags.map((tag) => <Tag key={tag}>{tag}</Tag>);
|
||||
};
|
||||
|
||||
export type ListFormProps = {
|
||||
onCancel: (flag?: boolean, formVals?: unknown) => void;
|
||||
@@ -26,6 +57,9 @@ export type ListFormProps = {
|
||||
experienceEnum: DictValueEnumObj;
|
||||
areaEnum: DictValueEnumObj;
|
||||
jobTypeEnum: DictValueEnumObj;
|
||||
salaryCompositionEnum: DictValueEnumObj;
|
||||
welfareBenefitsEnum: DictValueEnumObj;
|
||||
workScheduleEnum: DictValueEnumObj;
|
||||
mode?: 'view' | 'edit' | 'create';
|
||||
};
|
||||
|
||||
@@ -38,23 +72,28 @@ const waitTime = (time: number = 100) => {
|
||||
};
|
||||
|
||||
const listEdit: React.FC<ListFormProps> = (props) => {
|
||||
const [form] = Form.useForm<API.ManagementList.Manage>();
|
||||
const [form] = Form.useForm<ManageFormValues>();
|
||||
const companyNameMap = useRef<Record<number, string>>({});
|
||||
const [jobDetail, setJobDetail] = useState<API.ManagementList.Manage | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [jobCategoryTreeData, setJobCategoryTreeData] = useState<any[]>([]);
|
||||
const [mapOpen, setMapOpen] = useState<boolean>(false);
|
||||
const [mapViewInfo, setMapViewInfo] = useState<any>({});
|
||||
const { educationEnum, experienceEnum, areaEnum, jobTypeEnum } = props;
|
||||
const {
|
||||
educationEnum,
|
||||
experienceEnum,
|
||||
areaEnum,
|
||||
jobTypeEnum,
|
||||
salaryCompositionEnum,
|
||||
welfareBenefitsEnum,
|
||||
workScheduleEnum,
|
||||
} = props;
|
||||
const { mode = props.values ? 'edit' : 'create' } = props;
|
||||
useEffect(() => {
|
||||
if(props.open){
|
||||
form.resetFields();
|
||||
if (props.values) {
|
||||
form.setFieldsValue({
|
||||
...props.values,
|
||||
jobLocationAreaCode: String(props.values.jobLocationAreaCode || ''),
|
||||
});
|
||||
form.setFieldsValue(toManageFormValues(props.values));
|
||||
// 初始化地图位置信息(编辑时回显)
|
||||
if (props.values.latitude || props.values.longitude) {
|
||||
setMapViewInfo({
|
||||
@@ -92,10 +131,7 @@ const listEdit: React.FC<ListFormProps> = (props) => {
|
||||
setJobDetail(response.data);
|
||||
// 如果是编辑模式,将获取到的详情数据设置到表单中
|
||||
if (mode === 'edit') {
|
||||
form.setFieldsValue({
|
||||
...response.data,
|
||||
jobLocationAreaCode: String(response.data.jobLocationAreaCode || ''),
|
||||
});
|
||||
form.setFieldsValue(toManageFormValues(response.data));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -139,6 +175,9 @@ const listEdit: React.FC<ListFormProps> = (props) => {
|
||||
: companyNameMap.current[values.companyId] ?? '',
|
||||
minSalary: values.minSalary,
|
||||
maxSalary: values.maxSalary,
|
||||
salaryComposition: serializeCommaSeparatedTags(values.salaryComposition),
|
||||
welfareBenefits: serializeCommaSeparatedTags(values.welfareBenefits),
|
||||
workSchedule: serializeCommaSeparatedTags(values.workSchedule),
|
||||
education: values.education,
|
||||
experience: values.experience,
|
||||
jobLocationAreaCode: values.jobLocationAreaCode,
|
||||
@@ -189,6 +228,24 @@ const listEdit: React.FC<ListFormProps> = (props) => {
|
||||
/>
|
||||
<ProDescriptions.Item dataIndex="minSalary" label="最低薪资(元/月)" />
|
||||
<ProDescriptions.Item dataIndex="maxSalary" label="最高薪资(元/月)" />
|
||||
<ProDescriptions.Item
|
||||
dataIndex="salaryComposition"
|
||||
label="薪资范围与构成"
|
||||
span={2}
|
||||
render={(value) => renderDetailTags(value)}
|
||||
/>
|
||||
<ProDescriptions.Item
|
||||
dataIndex="welfareBenefits"
|
||||
label="福利待遇"
|
||||
span={2}
|
||||
render={(value) => renderDetailTags(value)}
|
||||
/>
|
||||
<ProDescriptions.Item
|
||||
dataIndex="workSchedule"
|
||||
label="工作时间安排"
|
||||
span={2}
|
||||
render={(value) => renderDetailTags(value)}
|
||||
/>
|
||||
<ProDescriptions.Item
|
||||
dataIndex="education"
|
||||
label="学历要求"
|
||||
@@ -341,7 +398,7 @@ const listEdit: React.FC<ListFormProps> = (props) => {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ModalForm<API.ManagementList.Manage>
|
||||
<ModalForm<ManageFormValues>
|
||||
title={mode === 'edit' ? '编辑岗位' : '新建岗位'}
|
||||
form={form}
|
||||
autoFocusFirstInput
|
||||
@@ -411,6 +468,33 @@ const listEdit: React.FC<ListFormProps> = (props) => {
|
||||
label="最大薪资(元/月)"
|
||||
placeholder="请输入最大薪资(元)"
|
||||
/>
|
||||
<ProFormSelect
|
||||
mode="tags"
|
||||
width="md"
|
||||
name="salaryComposition"
|
||||
label="薪资范围与构成"
|
||||
valueEnum={salaryCompositionEnum}
|
||||
placeholder="请选择或手动输入薪资构成"
|
||||
fieldProps={{ tokenSeparators: [',', ','], maxTagCount: 'responsive' }}
|
||||
/>
|
||||
<ProFormSelect
|
||||
mode="tags"
|
||||
width="md"
|
||||
name="welfareBenefits"
|
||||
label="福利待遇"
|
||||
valueEnum={welfareBenefitsEnum}
|
||||
placeholder="请选择或手动输入福利待遇"
|
||||
fieldProps={{ tokenSeparators: [',', ','], maxTagCount: 'responsive' }}
|
||||
/>
|
||||
<ProFormSelect
|
||||
mode="tags"
|
||||
width="md"
|
||||
name="workSchedule"
|
||||
label="工作时间安排"
|
||||
valueEnum={workScheduleEnum}
|
||||
placeholder="请选择或手动输入工作时间安排"
|
||||
fieldProps={{ tokenSeparators: [',', ','], maxTagCount: 'responsive' }}
|
||||
/>
|
||||
<ProFormSelect
|
||||
width="md"
|
||||
name="education"
|
||||
|
||||
@@ -85,6 +85,9 @@ function ManagementList() {
|
||||
const [isPublishEnum, setIsPublishEnum] = useState<any>([]);
|
||||
const [jobTypeEnum, setJobTypeEnum] = useState<any>([]);
|
||||
const [compensationEnum, setCompensationEnum] = useState<any>([]);
|
||||
const [salaryCompositionEnum, setSalaryCompositionEnum] = useState<any>([]);
|
||||
const [welfareBenefitsEnum, setWelfareBenefitsEnum] = useState<any>([]);
|
||||
const [workScheduleEnum, setWorkScheduleEnum] = useState<any>([]);
|
||||
const [currentRow, setCurrentRow] = useState<API.ManagementList.Manage>();
|
||||
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
||||
const [mode, setMode] = useState<'view' | 'edit' | 'create'>('create');
|
||||
@@ -107,6 +110,15 @@ function ManagementList() {
|
||||
getDictValueEnum('job_type',true,true).then((data) => {
|
||||
setJobTypeEnum(data)
|
||||
})
|
||||
getDictValueEnum('salary_composition', false, true).then((data) => {
|
||||
setSalaryCompositionEnum(data);
|
||||
});
|
||||
getDictValueEnum('welfare_benefits', false, true).then((data) => {
|
||||
setWelfareBenefitsEnum(data);
|
||||
});
|
||||
getDictValueEnum('work_schedule', false, true).then((data) => {
|
||||
setWorkScheduleEnum(data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
async function changeRelease(record: API.ManagementList.Manage) {
|
||||
@@ -492,6 +504,9 @@ function ManagementList() {
|
||||
experienceEnum={experienceEnum}
|
||||
areaEnum={areaEnum}
|
||||
jobTypeEnum={jobTypeEnum}
|
||||
salaryCompositionEnum={salaryCompositionEnum}
|
||||
welfareBenefitsEnum={welfareBenefitsEnum}
|
||||
workScheduleEnum={workScheduleEnum}
|
||||
></EditManageRow>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
@@ -58,3 +58,22 @@ export async function getCmsCompanyDetail(companyId: number | string) {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取当前企业用户关联的企业信息 */
|
||||
export async function getCmsCurrentCompany() {
|
||||
return request<{
|
||||
code: number;
|
||||
msg?: string;
|
||||
data?: API.CompanyList.Company;
|
||||
}>('/api/cms/company/current', {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
/** 修改当前企业用户关联的企业信息 */
|
||||
export async function putCmsCurrentCompany(params: API.CompanyList.Params) {
|
||||
return request<API.CompanyList.CompanyListResult>('/api/cms/company/current', {
|
||||
method: 'PUT',
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
||||
8
src/types/Management/list.d.ts
vendored
8
src/types/Management/list.d.ts
vendored
@@ -23,6 +23,9 @@ declare namespace API.ManagementList {
|
||||
longitude?: number;
|
||||
maxSalary?: number;
|
||||
minSalary?: number;
|
||||
salaryComposition?: string;
|
||||
welfareBenefits?: string;
|
||||
workSchedule?: string;
|
||||
postingDate?: string;
|
||||
vacancies?: number;
|
||||
view?: number;
|
||||
@@ -32,6 +35,7 @@ declare namespace API.ManagementList {
|
||||
jobContactList?: ContactPerson[];
|
||||
createTime?: string;
|
||||
jobCategory?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface AddParams {
|
||||
@@ -52,6 +56,9 @@ declare namespace API.ManagementList {
|
||||
longitude?: number;
|
||||
maxSalary?: number;
|
||||
minSalary?: number;
|
||||
salaryComposition?: string;
|
||||
welfareBenefits?: string;
|
||||
workSchedule?: string;
|
||||
postingDate?: string;
|
||||
vacancies?: number;
|
||||
view?: number;
|
||||
@@ -60,6 +67,7 @@ declare namespace API.ManagementList {
|
||||
jobType?: string;
|
||||
jobContactList?: ContactPerson[];
|
||||
jobCategory?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface ListParams {
|
||||
|
||||
11
src/types/company/list.d.ts
vendored
11
src/types/company/list.d.ts
vendored
@@ -9,7 +9,6 @@ declare namespace API.CompanyList {
|
||||
export interface CompanyContact {
|
||||
contactPerson: string;
|
||||
contactPersonPhone: string;
|
||||
position?: string;
|
||||
}
|
||||
|
||||
export interface Company {
|
||||
@@ -19,8 +18,12 @@ declare namespace API.CompanyList {
|
||||
location?: any;
|
||||
industry: string;
|
||||
scale: string;
|
||||
companyNature?: string;
|
||||
code: string;
|
||||
description: string;
|
||||
registeredAddress?: string;
|
||||
legalPerson?: string;
|
||||
legalPhone?: string;
|
||||
companyContactList?: CompanyContact[];
|
||||
contactPerson?: string;
|
||||
contactPersonPhone?: string;
|
||||
@@ -36,7 +39,13 @@ declare namespace API.CompanyList {
|
||||
location?: any;
|
||||
industry?: string;
|
||||
scale?: string;
|
||||
companyNature?: string;
|
||||
code?: string;
|
||||
description?: string;
|
||||
registeredAddress?: string;
|
||||
legalPerson?: string;
|
||||
legalPhone?: string;
|
||||
companyContactList?: CompanyContact[];
|
||||
status?: 0 | 1 | 2;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
|
||||
16
src/utils/jobDetailTags.ts
Normal file
16
src/utils/jobDetailTags.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const parseCommaSeparatedTags = (value: unknown): string[] => {
|
||||
const rawValues = Array.isArray(value)
|
||||
? value
|
||||
: typeof value === 'string'
|
||||
? value.split(/[,,]/)
|
||||
: [];
|
||||
|
||||
return Array.from(new Set(rawValues.map((item) => String(item ?? '').trim()).filter(Boolean)));
|
||||
};
|
||||
|
||||
export const serializeCommaSeparatedTags = (value: unknown): string | undefined => {
|
||||
if (value === null || value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return parseCommaSeparatedTags(value).join(',');
|
||||
};
|
||||
21
tests/utils/jobDetailTags.test.ts
Normal file
21
tests/utils/jobDetailTags.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { parseCommaSeparatedTags, serializeCommaSeparatedTags } from '@/utils/jobDetailTags';
|
||||
|
||||
describe('job detail tag helpers', () => {
|
||||
it('parses English and Chinese commas, trims values, and removes duplicates', () => {
|
||||
expect(parseCommaSeparatedTags('基本工资, 绩效工资,奖金,基本工资')).toEqual([
|
||||
'基本工资',
|
||||
'绩效工资',
|
||||
'奖金',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps dictionary selections and manual entries in one comma-separated value', () => {
|
||||
expect(serializeCommaSeparatedTags(['五险一金', '带薪年假', '生日福利'])).toBe(
|
||||
'五险一金,带薪年假,生日福利',
|
||||
);
|
||||
});
|
||||
|
||||
it('serializes a cleared selector as an empty string so an existing value can be removed', () => {
|
||||
expect(serializeCommaSeparatedTags([])).toBe('');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user