diff --git a/config/routes.ts b/config/routes.ts index c75e63b..565eb4b 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -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', diff --git a/src/components/JobComplaintModal/index.tsx b/src/components/JobComplaintModal/index.tsx index 4a2b168..30f3a80 100644 --- a/src/components/JobComplaintModal/index.tsx +++ b/src/components/JobComplaintModal/index.tsx @@ -117,7 +117,7 @@ const JobComplaintModal: React.FC = ({ open={open} onCancel={onCancel} width={600} - destroyOnClose + destroyOnHidden footer={ diff --git a/src/pages/Company/Info/index.tsx b/src/pages/Company/Info/index.tsx new file mode 100644 index 0000000..d77eb2d --- /dev/null +++ b/src/pages/Company/Info/index.tsx @@ -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(); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [scaleEnum, setScaleEnum] = useState({}); + const [companyNatureEnum, setCompanyNatureEnum] = useState({}); + const [industryTree, setIndustryTree] = useState([]); + + 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 ( + + + + form={form} + layout="vertical" + onFinish={handleFinish} + style={{ maxWidth: 1100 }} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }) => ( + + + + + + + + remove(name)} /> + + ))} + + + )} + + + + + + + + + + ); +}; + +export default CompanyInfoPage; diff --git a/src/pages/Company/List/edit.tsx b/src/pages/Company/List/edit.tsx index b41ee75..0aad5fa 100644 --- a/src/pages/Company/List/edit.tsx +++ b/src/pages/Company/List/edit.tsx @@ -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; scaleEnum?: DictValueEnumObj; + companyNatureEnum?: DictValueEnumObj; }; const ListEdit: React.FC = (props) => { @@ -80,7 +80,6 @@ const ListEdit: React.FC = (props) => { name="name" label="单位名称" placeholder="请输入单位名称" - disabled={!!props.values} /> = (props) => { valueEnum={props.scaleEnum} placeholder="请选择单位规模" rules={[{ required: true, message: '请选择单位规模!' }]} - disabled={!!props.values} /> + = (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 = (props) => { name="location" label="单位地点" placeholder="请输入单位地点" - disabled={!!props.values} /> - + + + + + {/* 企业联系人动态表单 */} @@ -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 !== '' && ( )} + {record?.companyNature != null && record.companyNature !== '' && ( + + )} {statusTag} {record?.industry != null && record.industry !== '' ? ( @@ -272,9 +299,25 @@ export const CompanyDetailView = ({ {renderDetailText(record?.location as string)} + + {record?.companyNature ? ( + + ) : ( + renderDetailText() + )} + {renderDetailText(record?.code)} + + {renderDetailText(record?.registeredAddress)} + + + {renderDetailText(record?.legalPerson)} + + + {renderDetailText(record?.legalPhone)} + @@ -307,12 +350,6 @@ export const CompanyDetailView = ({ {renderDetailText(contact.contactPersonPhone)} - {contact.position?.trim() ? ( -
- - {contact.position} -
- ) : null} ))} diff --git a/src/pages/Company/List/index.tsx b/src/pages/Company/List/index.tsx index c45dd35..40ee021 100644 --- a/src/pages/Company/List/index.tsx +++ b/src/pages/Company/List/index.tsx @@ -68,6 +68,7 @@ function ManagementList() { const [detailLoading, setDetailLoading] = useState(false); const [detailData, setDetailData] = useState(); const [scaleEnum, setScaleEnum] = useState>({}); + const [companyNatureEnum, setCompanyNatureEnum] = useState({}); const [industryEnum, setIndustryEnum] = useState({}); const [approvalVisible, setApprovalVisible] = useState(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 ; }, }, + { + title: '企业性质', + dataIndex: 'companyNature', + valueType: 'select', + align: 'center', + valueEnum: companyNatureEnum, + render: (_, record) => , + }, { 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} /> diff --git a/src/pages/JobPortal/Detail/index.less b/src/pages/JobPortal/Detail/index.less index e309ea7..30f11dd 100644 --- a/src/pages/JobPortal/Detail/index.less +++ b/src/pages/JobPortal/Detail/index.less @@ -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; + } } } diff --git a/src/pages/JobPortal/Detail/index.tsx b/src/pages/JobPortal/Detail/index.tsx index 339f835..45c1d55 100644 --- a/src/pages/JobPortal/Detail/index.tsx +++ b/src/pages/JobPortal/Detail/index.tsx @@ -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 || `

职位职责:

@@ -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: `

职位职责:

@@ -193,15 +202,27 @@ const JobDetailPage: React.FC = () => { { item: '工作地', score: 0 } ]); const [scaleEnum, setScaleEnum] = useState({}); + const [companyNatureEnum, setCompanyNatureEnum] = useState({}); const [industryTree, setIndustryTree] = useState([]); 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 = () => {
+ {jobDetail.companyNature && ( + + {getDictLabel(companyNatureEnum, jobDetail.companyNature)} + + )} {jobDetail.tags.map((tag, index) => ( {tag} ))} @@ -549,6 +580,23 @@ const JobDetailPage: React.FC = () => { {/* 左侧内容区 */} + {detailTagGroups.length > 0 && ( + +
+ {detailTagGroups.map((group) => ( +
+
{group.label}
+ + {group.values.map((value) => ( + {value} + ))} + +
+ ))} +
+
+ )} + {/* 职位描述 */}
{ }; export default JobDetailPage; - diff --git a/src/pages/JobPortal/List/index.less b/src/pages/JobPortal/List/index.less index 4e84c2e..33ca9d3 100644 --- a/src/pages/JobPortal/List/index.less +++ b/src/pages/JobPortal/List/index.less @@ -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; diff --git a/src/pages/JobPortal/List/index.tsx b/src/pages/JobPortal/List/index.tsx index 6f77075..bf5c156 100644 --- a/src/pages/JobPortal/List/index.tsx +++ b/src/pages/JobPortal/List/index.tsx @@ -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(false); const [jobCategory, setJobCategory] = useState(''); const [scaleEnum, setScaleEnum] = useState({}); + const [companyNatureEnum, setCompanyNatureEnum] = useState({}); const [jobTitleTree, setJobTitleTree] = useState([]); const [industryTree, setIndustryTree] = useState([]); const [searchValue, setSearchValue] = useState(''); // 搜索框的值 @@ -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 && ( {resolveJobCategoryLabel(job)} )} + {(job.companyNature || job.company?.companyNature) && ( + + {getDictLabel(companyNatureEnum, job.companyNature || job.company?.companyNature)} + + )} {/* {job.dataSource && {job.dataSource}} */} {job.vacancies && 招聘{job.vacancies}人} {/* {job.industry && {job.industry}} */} @@ -847,6 +913,14 @@ const JobListPage: React.FC = () => { {selectedJob?.jobCategory && ( {resolveJobCategoryLabel(selectedJob)} )} + {(selectedJob?.companyNature || selectedJob?.company?.companyNature) && ( + + {getDictLabel( + companyNatureEnum, + selectedJob.companyNature || selectedJob.company?.companyNature, + )} + + )} {selectedJob?.vacancies && ( 招聘{selectedJob.vacancies}人 )} @@ -891,6 +965,30 @@ const JobListPage: React.FC = () => { {/* 左侧内容区 */} + {selectedJobDetailTagGroups.length > 0 && ( +
+ + 岗位待遇与工作安排 + +
+ {selectedJobDetailTagGroups.map((group) => ( +
+
{group.label}
+ + {group.values.map((value) => ( + + {value} + + ))} + +
+ ))} +
+
+ )}
职位描述 @@ -1049,4 +1147,3 @@ const JobListPage: React.FC = () => { }; export default JobListPage; - diff --git a/src/pages/JobPortal/index.tsx b/src/pages/JobPortal/index.tsx index cceab6c..513da20 100644 --- a/src/pages/JobPortal/index.tsx +++ b/src/pages/JobPortal/index.tsx @@ -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(1); const [jobTitleData, setJobTitleData] = useState(null); const [jobRecommendData, setJobRecommendData] = useState(null); + const [companyNatureEnum, setCompanyNatureEnum] = useState({}); const [loading, setLoading] = useState(false); const [itemsPerPage] = useState(6); const [totalPages, setTotalPages] = useState(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 = () => {
{job.jobCategory && {job.jobCategory}} + {(job.companyNature || job.company?.companyNature) && ( + + {getDictLabel(companyNatureEnum, job.companyNature || job.company?.companyNature)} + + )} {job.vacancies && 招聘{job.vacancies}人}
diff --git a/src/pages/Management/List/edit.tsx b/src/pages/Management/List/edit.tsx index 0662de6..1c9de25 100644 --- a/src/pages/Management/List/edit.tsx +++ b/src/pages/Management/List/edit.tsx @@ -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, +): Partial => ({ + ...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 暂无; + } + return tags.map((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 = (props) => { - const [form] = Form.useForm(); + const [form] = Form.useForm(); const companyNameMap = useRef>({}); const [jobDetail, setJobDetail] = useState(null); const [loading, setLoading] = useState(false); const [jobCategoryTreeData, setJobCategoryTreeData] = useState([]); const [mapOpen, setMapOpen] = useState(false); const [mapViewInfo, setMapViewInfo] = useState({}); - 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 = (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 = (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 = (props) => { /> + renderDetailTags(value)} + /> + renderDetailTags(value)} + /> + renderDetailTags(value)} + /> = (props) => { ); } return ( - + title={mode === 'edit' ? '编辑岗位' : '新建岗位'} form={form} autoFocusFirstInput @@ -411,6 +468,33 @@ const listEdit: React.FC = (props) => { label="最大薪资(元/月)" placeholder="请输入最大薪资(元)" /> + + + ([]); const [jobTypeEnum, setJobTypeEnum] = useState([]); const [compensationEnum, setCompensationEnum] = useState([]); + const [salaryCompositionEnum, setSalaryCompositionEnum] = useState([]); + const [welfareBenefitsEnum, setWelfareBenefitsEnum] = useState([]); + const [workScheduleEnum, setWorkScheduleEnum] = useState([]); const [currentRow, setCurrentRow] = useState(); const [modalVisible, setModalVisible] = useState(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} > ); diff --git a/src/services/company/list.ts b/src/services/company/list.ts index f8572cf..3e38b30 100644 --- a/src/services/company/list.ts +++ b/src/services/company/list.ts @@ -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/cms/company/current', { + method: 'PUT', + data: params, + }); +} diff --git a/src/types/Management/list.d.ts b/src/types/Management/list.d.ts index b21b59f..7715495 100644 --- a/src/types/Management/list.d.ts +++ b/src/types/Management/list.d.ts @@ -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 { diff --git a/src/types/company/list.d.ts b/src/types/company/list.d.ts index 691575b..3d2a274 100644 --- a/src/types/company/list.d.ts +++ b/src/types/company/list.d.ts @@ -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; diff --git a/src/utils/jobDetailTags.ts b/src/utils/jobDetailTags.ts new file mode 100644 index 0000000..4d2d078 --- /dev/null +++ b/src/utils/jobDetailTags.ts @@ -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(','); +}; diff --git a/tests/utils/jobDetailTags.test.ts b/tests/utils/jobDetailTags.test.ts new file mode 100644 index 0000000..0e801b2 --- /dev/null +++ b/tests/utils/jobDetailTags.test.ts @@ -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(''); + }); +});