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:
2026-07-20 11:53:43 +08:00
parent 4d48d33476
commit ed0ac7c4c2
17 changed files with 681 additions and 47 deletions

View File

@@ -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;