From 9c8bfb617bbb05490416d78679c0fdf6b7b9a58e Mon Sep 17 00:00:00 2001 From: lapuda <577732344@qq.com> Date: Sat, 27 Jun 2026 22:45:16 +0800 Subject: [PATCH] feat: Enhance Outdoor Fair and Public Job Fair features - Added QR code functionality for companies signing up for outdoor fairs. - Implemented job management for outdoor fairs, including job listing and editing. - Updated Public Job Fair detail view to reflect new data structure. - Refactored EditModal to support dynamic lists for host, co-organizer, and organizing units. - Introduced cross-domain city selection for job fairs. - Enhanced API services for managing cross-domain jobs and cities. - Improved data handling for outdoor fair items, including review status and QR code content. - Updated type definitions to accommodate new fields and structures. --- src/pages/Jobfair/Crosscityfair/index.tsx | 1152 +++++++---------- .../Detail/components/DeviceTab.tsx | 74 +- .../Detail/components/JobEditModal.tsx | 27 +- .../components/ParticipatingCompaniesTab.tsx | 233 +++- .../components/ParticipatingJobsTab.tsx | 100 +- src/pages/Jobfair/Outdoorfair/index.tsx | 330 ++++- .../Jobfair/PublicJobFair/Detail/index.tsx | 7 +- .../PublicJobFair/components/EditModal.tsx | 136 +- src/pages/Jobfair/PublicJobFair/index.tsx | 9 + src/services/jobfair/crossCityAlliance.ts | 86 ++ src/services/jobfair/publicJobFair.ts | 43 + src/services/jobportal/outdoorFair.ts | 50 + src/services/jobportal/outdoorFairDetail.ts | 60 +- src/services/jobportal/outdoorFairDevice.ts | 24 +- src/types/jobfair/outdoorFairDetail.d.ts | 28 +- src/types/jobfair/publicJobFair.d.ts | 12 +- 16 files changed, 1613 insertions(+), 758 deletions(-) create mode 100644 src/services/jobfair/crossCityAlliance.ts diff --git a/src/pages/Jobfair/Crosscityfair/index.tsx b/src/pages/Jobfair/Crosscityfair/index.tsx index 2fbd139..63096bb 100644 --- a/src/pages/Jobfair/Crosscityfair/index.tsx +++ b/src/pages/Jobfair/Crosscityfair/index.tsx @@ -1,5 +1,13 @@ -import React, { useState } from 'react'; -import { PageContainer } from '@ant-design/pro-components'; +import React, { useEffect, useRef, useState } from 'react'; +import { useAccess } from '@umijs/max'; +import { + PageContainer, + ActionType, + ProColumns, + ProTable, + ModalForm, + ProFormText, +} from '@ant-design/pro-components'; import { Tabs, Card, @@ -10,588 +18,359 @@ import { Tag, Button, Space, - Descriptions, Typography, message, - Tooltip, + Modal, + Spin, } from 'antd'; import { EnvironmentOutlined, - StarOutlined, - StarFilled, - SendOutlined, - TeamOutlined, BankOutlined, FileTextOutlined, RiseOutlined, - CheckCircleOutlined, - EyeOutlined, + PlusOutlined, + DeleteOutlined, + EditOutlined, + SettingOutlined, } from '@ant-design/icons'; +import { getDictValueEnum } from '@/services/system/dict'; +import { + getCrossCityAllianceList, + addCrossCityAlliance, + updateCrossCityAlliance, + deleteCrossCityAlliance, +} from '@/services/jobfair/crossCityAlliance'; +import type { + CrossCityAllianceItem, + CrossCityAllianceForm, +} from '@/services/jobfair/crossCityAlliance'; +import { getCrossDomainJobs } from '@/services/jobfair/publicJobFair'; +import type { CrossDomainJobItem } from '@/services/jobfair/publicJobFair'; -const { Title, Paragraph, Text } = Typography; +const { Title, Text } = Typography; -// ==================== Mock Data ==================== +const PAGE_SIZE_FOR_STATS = 10000; -/** 联盟城市 */ -const ALLIANCE_CITIES = [ - { key: 'wulumuqi', name: '乌鲁木齐市', code: '650100', region: '北疆' }, - { key: 'kelamayi', name: '克拉玛依市', code: '650200', region: '北疆' }, - { key: 'changji', name: '昌吉回族自治州', code: '652300', region: '北疆' }, - { key: 'hami', name: '哈密市', code: '650500', region: '东疆' }, - { key: 'tulufan', name: '吐鲁番市', code: '650400', region: '东疆' }, - { key: 'kashgar', name: '喀什地区', code: '653100', region: '南疆' }, - { key: 'akesu', name: '阿克苏地区', code: '652900', region: '南疆' }, - { key: 'yili', name: '伊犁哈萨克自治州', code: '654000', region: '北疆' }, -]; - -/** 各城市招聘岗位 */ -interface CityJob { - id: number; - cityName: string; - cityKey: string; - companyName: string; - jobTitle: string; - salary: string; - education: string; - experience: string; - vacancies: number; - postDate: string; - industry: string; - saved: boolean; -} - -const MOCK_CITY_JOBS: CityJob[] = [ - { id: 1, cityName: '乌鲁木齐市', cityKey: 'wulumuqi', companyName: '新疆特变电工股份有限公司', jobTitle: '电气工程师', salary: '8K-12K', education: '本科', experience: '3-5年', vacancies: 5, postDate: '2026-06-15', industry: '制造业', saved: false }, - { id: 2, cityName: '乌鲁木齐市', cityKey: 'wulumuqi', companyName: '新疆广汇实业投资集团', jobTitle: '财务主管', salary: '10K-15K', education: '本科', experience: '5年以上', vacancies: 2, postDate: '2026-06-14', industry: '金融/投资', saved: false }, - { id: 3, cityName: '乌鲁木齐市', cityKey: 'wulumuqi', companyName: '中国石油新疆油田分公司', jobTitle: '石油工程技术员', salary: '9K-14K', education: '本科', experience: '1-3年', vacancies: 10, postDate: '2026-06-13', industry: '能源/矿产', saved: true }, - { id: 4, cityName: '克拉玛依市', cityKey: 'kelamayi', companyName: '克拉玛依石化公司', jobTitle: '化工工艺工程师', salary: '10K-16K', education: '硕士', experience: '3-5年', vacancies: 3, postDate: '2026-06-16', industry: '石化', saved: false }, - { id: 5, cityName: '克拉玛依市', cityKey: 'kelamayi', companyName: '新疆油田公司勘探开发研究院', jobTitle: '地质研究员', salary: '12K-18K', education: '博士', experience: '不限', vacancies: 2, postDate: '2026-06-12', industry: '能源/矿产', saved: false }, - { id: 6, cityName: '克拉玛依市', cityKey: 'kelamayi', companyName: '克拉玛依职业技术学院', jobTitle: '机械专业教师', salary: '7K-10K', education: '硕士', experience: '1-3年', vacancies: 4, postDate: '2026-06-10', industry: '教育', saved: true }, - { id: 7, cityName: '昌吉回族自治州', cityKey: 'changji', companyName: '特变电工新疆新能源股份有限公司', jobTitle: '光伏系统工程师', salary: '8K-13K', education: '本科', experience: '3-5年', vacancies: 6, postDate: '2026-06-15', industry: '新能源', saved: false }, - { id: 8, cityName: '昌吉回族自治州', cityKey: 'changji', companyName: '新疆蓝山屯河化工股份有限公司', jobTitle: '高分子材料研发', salary: '9K-14K', education: '硕士', experience: '1-3年', vacancies: 3, postDate: '2026-06-11', industry: '新材料', saved: false }, - { id: 9, cityName: '哈密市', cityKey: 'hami', companyName: '哈密市城市建设投资集团', jobTitle: '工程项目经理', salary: '12K-18K', education: '本科', experience: '5年以上', vacancies: 2, postDate: '2026-06-14', industry: '建筑/工程', saved: false }, - { id: 10, cityName: '哈密市', cityKey: 'hami', companyName: '新疆哈密风电基地', jobTitle: '风电运维工程师', salary: '7K-11K', education: '大专', experience: '1-3年', vacancies: 8, postDate: '2026-06-13', industry: '新能源', saved: true }, - { id: 11, cityName: '吐鲁番市', cityKey: 'tulufan', companyName: '吐鲁番市旅游发展集团', jobTitle: '文旅策划专员', salary: '5K-8K', education: '本科', experience: '1-3年', vacancies: 3, postDate: '2026-06-12', industry: '旅游/文化', saved: false }, - { id: 12, cityName: '喀什地区', cityKey: 'kashgar', companyName: '喀什经济开发区投资集团', jobTitle: '国际贸易专员', salary: '6K-10K', education: '本科', experience: '1-3年', vacancies: 5, postDate: '2026-06-16', industry: '贸易/进出口', saved: false }, - { id: 13, cityName: '喀什地区', cityKey: 'kashgar', companyName: '喀什地区人民医院', jobTitle: '临床医生', salary: '8K-13K', education: '硕士', experience: '不限', vacancies: 15, postDate: '2026-06-10', industry: '医疗/卫生', saved: false }, - { id: 14, cityName: '阿克苏地区', cityKey: 'akesu', companyName: '阿克苏纺织工业城', jobTitle: '纺织技术主管', salary: '7K-10K', education: '本科', experience: '3-5年', vacancies: 4, postDate: '2026-06-09', industry: '纺织/服装', saved: true }, - { id: 15, cityName: '阿克苏地区', cityKey: 'akesu', companyName: '阿克苏苹果产业集团', jobTitle: '食品加工工程师', salary: '6K-9K', education: '本科', experience: '1-3年', vacancies: 6, postDate: '2026-06-08', industry: '食品加工', saved: false }, - { id: 16, cityName: '伊犁哈萨克自治州', cityKey: 'yili', companyName: '伊犁州霍尔果斯口岸管委会', jobTitle: '跨境物流经理', salary: '10K-15K', education: '本科', experience: '5年以上', vacancies: 1, postDate: '2026-06-15', industry: '物流/交通', saved: false }, - { id: 17, cityName: '伊犁哈萨克自治州', cityKey: 'yili', companyName: '伊犁州文旅集团', jobTitle: '景区运营管理', salary: '6K-9K', education: '大专', experience: '3-5年', vacancies: 3, postDate: '2026-06-07', industry: '旅游/文化', saved: false }, - { id: 18, cityName: '乌鲁木齐市', cityKey: 'wulumuqi', companyName: '新疆医科大学附属医院', jobTitle: '护士长', salary: '8K-11K', education: '本科', experience: '5年以上', vacancies: 3, postDate: '2026-06-16', industry: '医疗/卫生', saved: true }, -]; - -/** 已收藏/投递的岗位 */ -interface SavedJob { - id: number; - cityName: string; - companyName: string; - jobTitle: string; - salary: string; - savedDate: string; - deliverStatus: 'pending' | 'delivered' | 'interview' | 'offer'; - deliverDate?: string; -} - -const MOCK_SAVED_JOBS: SavedJob[] = [ - { id: 1, cityName: '乌鲁木齐市', companyName: '中国石油新疆油田分公司', jobTitle: '石油工程技术员', salary: '9K-14K', savedDate: '2026-06-15', deliverStatus: 'delivered', deliverDate: '2026-06-16' }, - { id: 2, cityName: '克拉玛依市', companyName: '克拉玛依职业技术学院', jobTitle: '机械专业教师', salary: '7K-10K', savedDate: '2026-06-14', deliverStatus: 'interview', deliverDate: '2026-06-17' }, - { id: 3, cityName: '哈密市', companyName: '新疆哈密风电基地', jobTitle: '风电运维工程师', salary: '7K-11K', savedDate: '2026-06-13', deliverStatus: 'offer', deliverDate: '2026-06-18' }, - { id: 4, cityName: '阿克苏地区', companyName: '阿克苏纺织工业城', jobTitle: '纺织技术主管', salary: '7K-10K', savedDate: '2026-06-12', deliverStatus: 'pending' }, - { id: 5, cityName: '乌鲁木齐市', companyName: '新疆医科大学附属医院', jobTitle: '护士长', salary: '8K-11K', savedDate: '2026-06-16', deliverStatus: 'pending' }, - { id: 6, cityName: '昌吉回族自治州', companyName: '新疆蓝山屯河化工股份有限公司', jobTitle: '高分子材料研发', salary: '9K-14K', savedDate: '2026-06-11', deliverStatus: 'delivered', deliverDate: '2026-06-12' }, -]; - -/** 统计数据 */ -const STATISTICS_DATA = { - totalCities: 8, - totalCompanies: 156, - totalJobs: 2340, - totalJobSeekers: 12860, - totalResumeDelivered: 4560, - matchSuccessRate: 38.5, - industryDistribution: [ - { industry: '制造业', count: 45, percentage: 28.8 }, - { industry: '能源/矿产', count: 28, percentage: 17.9 }, - { industry: '医疗/卫生', count: 22, percentage: 14.1 }, - { industry: '教育/科研', count: 18, percentage: 11.5 }, - { industry: '建筑/工程', count: 15, percentage: 9.6 }, - { industry: '新能源', count: 12, percentage: 7.7 }, - { industry: '金融/投资', count: 10, percentage: 6.4 }, - { industry: '其他', count: 6, percentage: 3.8 }, - ], - cityJobDistribution: [ - { city: '乌鲁木齐市', jobs: 680, companies: 45 }, - { city: '克拉玛依市', jobs: 420, companies: 28 }, - { city: '昌吉回族自治州', jobs: 350, companies: 22 }, - { city: '哈密市', jobs: 280, companies: 18 }, - { city: '吐鲁番市', jobs: 160, companies: 12 }, - { city: '喀什地区', jobs: 200, companies: 15 }, - { city: '阿克苏地区', jobs: 150, companies: 10 }, - { city: '伊犁哈萨克自治州', jobs: 100, companies: 6 }, - ], - monthlyDelivery: [ - { month: '2026-01', count: 320 }, - { month: '2026-02', count: 280 }, - { month: '2026-03', count: 450 }, - { month: '2026-04', count: 520 }, - { month: '2026-05', count: 610 }, - { month: '2026-06', count: 780 }, - ], +const getValueEnumText = (valueEnum: any, value?: string | number) => { + if (value === undefined || value === null || value === '') return '-'; + const option = valueEnum[String(value)] || valueEnum[Number(value)]; + return option?.text || option?.label || String(value); }; -const STATUS_MAP: Record = { - pending: { color: 'default', label: '待投递' }, - delivered: { color: 'processing', label: '已投递' }, - interview: { color: 'warning', label: '面试中' }, - offer: { color: 'success', label: '已获Offer' }, +const formatSalary = (minSalary?: number, maxSalary?: number) => { + if (minSalary == null && maxSalary == null) return '-'; + if (minSalary != null && maxSalary != null) return `${minSalary}-${maxSalary} 元`; + if (minSalary != null) return `${minSalary} 元起`; + return `${maxSalary} 元以内`; }; -// ==================== 功能背景 Tab ==================== +// ==================== 联盟城市管理 弹窗 ==================== -const BackgroundTab: React.FC = () => ( -
- - - <TeamOutlined style={{ marginRight: 8 }} /> - 功能背景 - - - 石河子市人力资源和社会保障局为深入贯彻国家关于促进高质量充分就业的决策部署, - 积极与新疆维吾尔自治区其他地市联合举办跨域联合招聘活动,整合各地市就业服务资源, - 实现资源优势互补,为广大求职者提供更多地区的职位信息,为企业推送更多的优秀人才。 - - - 活动结束后,在当地投递的属于石河子地区的岗位数据、人才简历等相关数据 - 将统一回流至"石城智慧就业"一体化平台,实现数据有效归集和深度应用。 - - +interface AllianceManageModalProps { + open: boolean; + onClose: () => void; + onChanged: () => void | Promise; +} - - - - } - suffix="个" - /> -
- {ALLIANCE_CITIES.map((city) => ( - +const AllianceManageModal: React.FC = ({ open, onClose, onChanged }) => { + const access = useAccess(); + const actionRef = useRef(); + const [formOpen, setFormOpen] = useState(false); + const [currentRow, setCurrentRow] = useState(); + + const handleDelete = (id: number) => { + Modal.confirm({ + title: '确认删除', + content: '确定要删除该联盟城市吗?', + onOk: async () => { + const res = await deleteCrossCityAlliance(id); + if (res.code === 200) { + message.success('删除成功'); + actionRef.current?.reload(); + onChanged(); + } else { + message.error(res.msg || '删除失败'); + } + }, + }); + }; + + const columns: ProColumns[] = [ + { title: '城市名称', dataIndex: 'name', ellipsis: true }, + { + title: '操作', + valueType: 'option', + width: 160, + fixed: 'right', + render: (_, record) => [ + , + , + ], + }, + ]; + + return ( + <> + + + actionRef={actionRef} + rowKey="id" + headerTitle="联盟城市列表" + search={{ labelWidth: 'auto' }} + options={false} + pagination={{ pageSize: 10 }} + scroll={{ x: 'max-content' }} + toolBarRender={() => [ + , + ]} + request={async (params) => { + const res = await getCrossCityAllianceList({ + name: params.name, + current: params.current, + pageSize: params.pageSize, + }); + return { data: res.rows || [], total: res.total || 0, success: res.code === 200 }; + }} + columns={columns} + /> + + + + title={currentRow ? '编辑联盟城市' : '新增联盟城市'} + open={formOpen} + modalProps={{ destroyOnClose: true, onCancel: () => setFormOpen(false) }} + initialValues={ + currentRow + ? { + id: currentRow.id, + name: currentRow.name, + } + : undefined + } + onFinish={async (values) => { + const res = values.id + ? await updateCrossCityAlliance(values) + : await addCrossCityAlliance(values); + if (res.code === 200) { + message.success(values.id ? '修改成功' : '新增成功'); + setFormOpen(false); + setCurrentRow(undefined); + actionRef.current?.reload(); + onChanged(); + return true; + } + message.error(res.msg || '操作失败'); + return false; + }} + > + + + + ); +}; + +// ==================== 区域管理 Tab ==================== + +interface CitySelectTabProps { + cities: CrossCityAllianceItem[]; + citiesLoading: boolean; + selectedCities: string[]; + onToggle: (name: string) => void; + onClear: () => void; + onManage: () => void; +} + +const CitySelectTab: React.FC = ({ + cities, + citiesLoading, + selectedCities, + onToggle, + onClear, + onManage, +}) => { + const access = useAccess(); + return ( +
+ +
+ + <EnvironmentOutlined style={{ marginRight: 8 }} /> + 区域管理 + + {access.hasPerms('cms:crossCityFair:list') && ( + + )} +
+ + + {cities.map((city) => ( + onToggle(city.name)} + style={{ + padding: '4px 12px', + fontSize: 14, + borderRadius: 4, + }} + > {city.name} - + ))} -
- - - - - } - suffix="家" - /> -
- 覆盖制造业、能源、医疗、教育、建筑等多个重点行业 -
-
- - - - } - suffix="个" - /> -
- 涵盖技术类、管理类、一线工人等多种岗位类型 -
-
- - - - - 核心功能 - - - - - 一站式获取各地市就业信息,自由选择意向就业城市,查看招聘企业信息和用工信息。 - 聚合各地市就业资源,便捷获取全国范围内的工作机会。 - - - - - - - 一键收藏其他城市招聘企业和职位信息,在个人中心随时查看, - 跨区域快速投递简历,简化投递流程,提高求职效率。 - - - - - - - 自动汇总招聘会相关数据,包含参会企业数量、岗位数量、求职者人数、 - 简历投递数量、匹配成功率等,形成详细数据报表。 - - - - - - - 活动结束后,石河子地区岗位数据和人才简历自动回流至 - "石城智慧就业"一体化平台,实现数据归集和深度应用。 - - - - - -
-); + {selectedCities.length > 0 && ( + + )} + + +
+ 已选择 {selectedCities.length > 0 ? selectedCities.length + ' 个区域' : '全部区域'} + (选择区域后,到「各地市就业信息」查看对应岗位) +
+
+
+ ); +}; // ==================== 各地市就业信息 Tab ==================== -const CityJobsTab: React.FC = () => { - const [jobs, setJobs] = useState(MOCK_CITY_JOBS); - const [selectedCities, setSelectedCities] = useState([]); +const JobsTab: React.FC<{ selectedCities: string[] }> = ({ selectedCities }) => { + const actionRef = useRef(); + const cityNames = selectedCities.join(','); + const [educationEnum, setEducationEnum] = useState({}); + const [experienceEnum, setExperienceEnum] = useState({}); - const handleToggleSave = (id: number) => { - setJobs((prev) => - prev.map((job) => - job.id === id ? { ...job, saved: !job.saved } : job - ) - ); - const job = jobs.find((j) => j.id === id); - if (job) { - message.success(job.saved ? '已取消收藏' : '已收藏到个人中心'); - } - }; + useEffect(() => { + Promise.all([ + getDictValueEnum('education', true, true), + getDictValueEnum('experience', true, true), + ]).then(([education, experience]) => { + setEducationEnum(education); + setExperienceEnum(experience); + }); + }, []); - const handleQuickApply = (record: CityJob) => { - message.success(`已向「${record.companyName}」投递简历,岗位:${record.jobTitle}`); - }; - - const filteredJobs = selectedCities.length > 0 - ? jobs.filter((j) => selectedCities.includes(j.cityKey)) - : jobs; - - const cityColumns = [ - { - title: '城市', - dataIndex: 'cityName', - width: 120, - render: (text: string) => ( - - - {text} - - ), - }, + const columns: ProColumns[] = [ { title: '企业名称', dataIndex: 'companyName', ellipsis: true, - width: 200, + width: 220, }, { title: '职位名称', dataIndex: 'jobTitle', width: 160, - render: (text: string) => {text}, + render: (text: any) => {text}, }, { title: '薪资范围', - dataIndex: 'salary', - width: 100, - render: (text: string) => {text}, + dataIndex: 'minSalary', + width: 110, + render: (_: any, r: CrossDomainJobItem) => { + const salary = formatSalary(r.minSalary, r.maxSalary); + return salary === '-' ? '-' : {salary}; + }, }, { title: '学历要求', dataIndex: 'education', width: 90, + render: (_, record) => getValueEnumText(educationEnum, record.education), }, { title: '经验要求', dataIndex: 'experience', width: 90, + render: (_, record) => getValueEnumText(experienceEnum, record.experience), }, { title: '招聘人数', dataIndex: 'vacancies', width: 80, - align: 'center' as const, + align: 'center', }, { title: '行业', dataIndex: 'industry', width: 100, - render: (text: string) => {text}, - }, - { - title: '发布日期', - dataIndex: 'postDate', - width: 100, - }, - { - title: '操作', - width: 200, - fixed: 'right' as const, - render: (_: unknown, record: CityJob) => ( - - - - - ), + render: (text: any) => (text ? {text} : '-'), }, + { title: '发布日期', dataIndex: 'postingDate', width: 110, valueType: 'date' }, ]; return (
- - - <EnvironmentOutlined style={{ marginRight: 8 }} /> - 选择联盟城市 - - - {ALLIANCE_CITIES.map((city) => ( - { - setSelectedCities((prev) => - checked - ? [...prev, city.key] - : prev.filter((k) => k !== city.key) - ); - }} - style={{ - padding: '4px 12px', - fontSize: 14, - borderRadius: 4, - }} - > - {city.name} - - ))} - {selectedCities.length > 0 && ( - - )} - -
- 已选择 {selectedCities.length > 0 ? selectedCities.length + ' 个城市' : '全部城市'}, - 共 {filteredJobs.length} 个职位 + {selectedCities.length > 0 && ( +
+ 已选择 {selectedCities.length} 个区域({selectedCities.join('、')})
- - - + actionRef={actionRef} + rowKey="jobId" + search={false} + options={false} + pagination={{ pageSize: 10, showTotal: (t) => `共 ${t} 个职位` }} scroll={{ x: 'max-content' }} - pagination={{ - pageSize: 10, - showTotal: (total) => `共 ${total} 个职位`, + params={{ cityNames }} + request={async (params) => { + const res = await getCrossDomainJobs({ + cityNames: params.cityNames, + current: params.current, + pageSize: params.pageSize, + }); + return { data: res.rows || [], total: res.total || 0, success: res.code === 200 }; }} - /> - - ); -}; - -// ==================== 跨区简历投递 Tab ==================== - -const ResumeDeliveryTab: React.FC = () => { - const [savedJobs, setSavedJobs] = useState(MOCK_SAVED_JOBS); - - const handleDeliver = (record: SavedJob) => { - setSavedJobs((prev) => - prev.map((job) => - job.id === record.id - ? { ...job, deliverStatus: 'delivered' as const, deliverDate: '2026-06-18' } - : job - ) - ); - message.success(`简历已投递至「${record.companyName}」- ${record.jobTitle}`); - }; - - const columns = [ - { - title: '城市', - dataIndex: 'cityName', - width: 100, - render: (text: string) => ( - - - {text} - - ), - }, - { - title: '企业名称', - dataIndex: 'companyName', - ellipsis: true, - width: 180, - }, - { - title: '职位名称', - dataIndex: 'jobTitle', - width: 150, - render: (text: string) => {text}, - }, - { - title: '薪资', - dataIndex: 'salary', - width: 90, - render: (text: string) => {text}, - }, - { - title: '收藏时间', - dataIndex: 'savedDate', - width: 100, - }, - { - title: '投递状态', - dataIndex: 'deliverStatus', - width: 110, - render: (status: string) => { - const s = STATUS_MAP[status]; - return {s?.label}; - }, - }, - { - title: '投递时间', - dataIndex: 'deliverDate', - width: 100, - render: (text: string | undefined) => text || '--', - }, - { - title: '操作', - width: 140, - fixed: 'right' as const, - render: (_: unknown, record: SavedJob) => ( - - {record.deliverStatus === 'pending' && ( - - )} - {record.deliverStatus === 'delivered' && ( - - )} - {record.deliverStatus === 'interview' && ( - - )} - {record.deliverStatus === 'offer' && ( - - )} - - ), - }, - ]; - - return ( -
- -
- - } - suffix="个" - /> - - - - - j.deliverStatus !== 'pending').length} - prefix={} - suffix="份" - /> - - - - - j.deliverStatus === 'interview').length} - prefix={} - suffix="个" - /> - - - - - j.deliverStatus === 'offer').length} - prefix={} - suffix="个" - valueStyle={{ color: '#52c41a' }} - /> - - - - - - - - 在"各地市就业信息"中点击星标即可收藏心仪职位 - - - 收藏后在此页面一键投递简历,无需重复填写 - - - 实时查看简历投递状态:待投递 → 已投递 → 面试中 → 已获Offer - - - 投递石河子地区岗位的数据将自动回流至一体化平台 - - - - -
); @@ -600,160 +379,190 @@ const ResumeDeliveryTab: React.FC = () => { // ==================== 数据统计 Tab ==================== const DataStatsTab: React.FC = () => { + const [loading, setLoading] = useState(false); + const [stats, setStats] = useState({ + totalCities: 0, + totalCompanies: 0, + totalJobs: 0, + totalVacancies: 0, + industryDistribution: [] as { industry: string; count: number; percentage: string }[], + monthlyJobs: [] as { month: string; count: number }[], + }); + + useEffect(() => { + const loadStats = async () => { + setLoading(true); + try { + const [cityRes, jobRes] = await Promise.all([ + getCrossCityAllianceList({ pageSize: PAGE_SIZE_FOR_STATS }), + getCrossDomainJobs({ current: 1, pageSize: PAGE_SIZE_FOR_STATS }), + ]); + const jobs = jobRes.rows || []; + const companyIds = new Set( + jobs + .map((job) => job.companyId) + .filter((companyId) => companyId !== undefined && companyId !== null), + ); + const totalJobs = jobRes.total || jobs.length; + const industryCount = jobs.reduce>((acc, job) => { + const industry = job.industry || '其他'; + acc[industry] = (acc[industry] || 0) + 1; + return acc; + }, {}); + const monthlyCount = jobs.reduce>((acc, job) => { + if (!job.postingDate) return acc; + const month = String(job.postingDate).slice(0, 7); + acc[month] = (acc[month] || 0) + 1; + return acc; + }, {}); + + setStats({ + totalCities: cityRes.total || cityRes.rows?.length || 0, + totalCompanies: companyIds.size, + totalJobs, + totalVacancies: jobs.reduce((sum, job) => sum + (Number(job.vacancies) || 0), 0), + industryDistribution: Object.entries(industryCount).map(([industry, count]) => ({ + industry, + count, + percentage: totalJobs > 0 ? ((count / totalJobs) * 100).toFixed(1) : '0.0', + })), + monthlyJobs: Object.entries(monthlyCount) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([month, count]) => ({ month, count })), + }); + } finally { + setLoading(false); + } + }; + loadStats(); + }, []); + const industryColumns = [ { title: '行业', dataIndex: 'industry', key: 'industry' }, - { title: '企业数量', dataIndex: 'count', key: 'count', align: 'center' as const }, + { title: '岗位数量', dataIndex: 'count', key: 'count', align: 'center' as const }, { title: '占比', dataIndex: 'percentage', key: 'percentage', align: 'center' as const, - render: (val: number) => `${val}%`, + render: (val: string) => `${val}%`, }, ]; - const cityColumns = [ - { title: '城市', dataIndex: 'city', key: 'city' }, - { title: '岗位数量', dataIndex: 'jobs', key: 'jobs', align: 'center' as const }, - { title: '企业数量', dataIndex: 'companies', key: 'companies', align: 'center' as const }, - ]; - - const deliveryColumns = [ + const monthlyColumns = [ { title: '月份', dataIndex: 'month', key: 'month' }, - { title: '简历投递量', dataIndex: 'count', key: 'count', align: 'center' as const }, + { title: '发布岗位数', dataIndex: 'count', key: 'count', align: 'center' as const }, ]; return ( -
- -
- - } - suffix="个" - /> - - - - - } - suffix="家" - /> - - - - - } - suffix="个" - /> - - - + +
+ +
+ + } + suffix="个" + /> + + + + + } + suffix="家" + /> + + + + + } + suffix="个" + /> + + + + + } + suffix="人" + /> + + + - - - - } - suffix="人" - /> - - - - - } - suffix="份" - /> - - - - - } - suffix="次" - /> - - - - - } - suffix="%" - valueStyle={{ color: '#52c41a' }} - /> - - - - - - - -
- - - - -
- - - - - -
- - + + + +
+ + + + +
+ + + + + ); }; // ==================== 主页面 ==================== const CrossCityFair: React.FC = () => { + const [selectedCities, setSelectedCities] = useState([]); + const [cities, setCities] = useState([]); + const [citiesLoading, setCitiesLoading] = useState(false); + const [manageOpen, setManageOpen] = useState(false); + + const reloadCities = async () => { + setCitiesLoading(true); + try { + const res = await getCrossCityAllianceList({ pageSize: 1000 }); + if (res.code === 200) { + setCities(res.rows || []); + // 清理已不存在的城市筛选 + const validNames = new Set((res.rows || []).map((c) => c.name)); + setSelectedCities((prev) => prev.filter((k) => validNames.has(k))); + } + } finally { + setCitiesLoading(false); + } + }; + + useEffect(() => { + reloadCities(); + }, []); + + const toggleCity = (name: string) => { + setSelectedCities((prev) => + prev.includes(name) ? prev.filter((k) => k !== name) : [...prev, name], + ); + }; + const tabItems = [ - { - key: 'background', - label: ( - - - 功能背景 - - ), - children: , - }, { key: 'city-jobs', label: ( @@ -762,17 +571,7 @@ const CrossCityFair: React.FC = () => { 各地市就业信息 ), - children: , - }, - { - key: 'resume-delivery', - label: ( - - - 跨区简历投递 - - ), - children: , + children: , }, { key: 'statistics', @@ -784,6 +583,25 @@ const CrossCityFair: React.FC = () => { ), children: , }, + { + key: 'city-select', + label: ( + + + 区域管理 + + ), + children: ( + setSelectedCities([])} + onManage={() => setManageOpen(true)} + /> + ), + }, ]; return ( @@ -794,12 +612,14 @@ const CrossCityFair: React.FC = () => { }} > - + + + setManageOpen(false)} + onChanged={reloadCities} + /> ); }; diff --git a/src/pages/Jobfair/Outdoorfair/Detail/components/DeviceTab.tsx b/src/pages/Jobfair/Outdoorfair/Detail/components/DeviceTab.tsx index 0b22ea6..1b9002a 100644 --- a/src/pages/Jobfair/Outdoorfair/Detail/components/DeviceTab.tsx +++ b/src/pages/Jobfair/Outdoorfair/Detail/components/DeviceTab.tsx @@ -6,13 +6,13 @@ import { ProColumns, ProTable, ModalForm, - ProFormDigit, + ProFormText, ProFormSelect, } from '@ant-design/pro-components'; import { PlusOutlined, DeleteOutlined, LinkOutlined, DisconnectOutlined } from '@ant-design/icons'; import { getOutdoorFairDeviceList, - batchGenerateOutdoorFairDevice, + addOutdoorFairDevice, bindOutdoorFairDevice, deleteOutdoorFairDevice, getOutdoorFairCompanyOptions, @@ -26,7 +26,7 @@ interface Props { const DeviceTab: React.FC = ({ fairId }) => { const access = useAccess(); const actionRef = useRef(); - const [batchVisible, setBatchVisible] = useState(false); + const [addVisible, setAddVisible] = useState(false); const [bindVisible, setBindVisible] = useState(false); const [currentRow, setCurrentRow] = useState(); @@ -145,13 +145,13 @@ const DeviceTab: React.FC = ({ fairId }) => { scroll={{ x: 'max-content' }} toolBarRender={() => [ , ]} request={async (params) => { @@ -167,34 +167,60 @@ const DeviceTab: React.FC = ({ fairId }) => { columns={columns} /> - - title="批量生成设备码" - open={batchVisible} - modalProps={{ destroyOnClose: true, onCancel: () => setBatchVisible(false) }} - initialValues={{ quantity: 10 }} + + title="新增设备码" + open={addVisible} + modalProps={{ destroyOnClose: true, onCancel: () => setAddVisible(false) }} onFinish={async (values) => { - const res = await batchGenerateOutdoorFairDevice({ + const res = await addOutdoorFairDevice({ fairId, - quantity: Number(values.quantity), + deviceCode: values.deviceCode.trim(), + companyId: values.companyId, }); if (res.code === 200) { - message.success(`成功生成 ${res.data?.length || values.quantity} 个设备码`); - setBatchVisible(false); + message.success('新增成功'); + setAddVisible(false); actionRef.current?.reload(); return true; } - message.error(res.msg || '生成失败'); + message.error(res.msg || '新增失败'); return false; }} > - + { + const [companies, devicesRes] = await Promise.all([ + getOutdoorFairCompanyOptions(fairId), + getOutdoorFairDeviceList({ fairId, current: 1, pageSize: 1000 }), + ]); + // 排除已绑定到其他设备的企业 + const boundIds = new Set( + (devicesRes.rows || []) + .map((d) => d.companyId) + .filter((cid): cid is number => cid != null), + ); + return companies + .filter((item) => !boundIds.has(item.companyId)) + .map((item) => ({ + label: item.companyName, + value: item.companyId, + })); + }} /> diff --git a/src/pages/Jobfair/Outdoorfair/Detail/components/JobEditModal.tsx b/src/pages/Jobfair/Outdoorfair/Detail/components/JobEditModal.tsx index 6e1a52d..10cd660 100644 --- a/src/pages/Jobfair/Outdoorfair/Detail/components/JobEditModal.tsx +++ b/src/pages/Jobfair/Outdoorfair/Detail/components/JobEditModal.tsx @@ -10,7 +10,11 @@ import { import { Button, Form, Input, message } from 'antd'; import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import type { DictValueEnumObj } from '@/components/DictTag'; -import { addJobForCompany, updateJobForCompany } from '@/services/jobportal/outdoorFairDetail'; +import { + addCurrentCompanyJob, + addJobForCompany, + updateJobForCompany, +} from '@/services/jobportal/outdoorFairDetail'; interface Props { open: boolean; @@ -21,6 +25,7 @@ interface Props { experienceEnum: DictValueEnumObj; areaEnum: DictValueEnumObj; jobTypeEnum: DictValueEnumObj; + companyScoped?: boolean; onCancel: () => void; onSuccess: () => void; } @@ -34,6 +39,7 @@ const JobEditModal: React.FC = ({ experienceEnum, areaEnum, jobTypeEnum, + companyScoped, onCancel, onSuccess, }) => { @@ -82,7 +88,9 @@ const JobEditModal: React.FC = ({ }; const res = job ? await updateJobForCompany(fairId, { ...job, ...payload }) - : await addJobForCompany({ fairId, companyId: company!.companyId, ...payload }); + : companyScoped + ? await addCurrentCompanyJob({ fairId, ...payload }) + : await addJobForCompany({ fairId, companyId: company!.companyId, ...payload }); if (res.code === 200) { message.success(job ? '岗位修改成功' : '岗位添加成功'); onSuccess(); @@ -163,7 +171,13 @@ const JobEditModal: React.FC = ({ {fields.map(({ key, name, ...restField }) => (
= ({ > -
))} diff --git a/src/pages/Jobfair/Outdoorfair/Detail/components/ParticipatingCompaniesTab.tsx b/src/pages/Jobfair/Outdoorfair/Detail/components/ParticipatingCompaniesTab.tsx index 1b38e97..a7e15b5 100644 --- a/src/pages/Jobfair/Outdoorfair/Detail/components/ParticipatingCompaniesTab.tsx +++ b/src/pages/Jobfair/Outdoorfair/Detail/components/ParticipatingCompaniesTab.tsx @@ -1,15 +1,18 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Button, Modal, Tag, message } from 'antd'; +import { Button, Form, Input, Modal, QRCode, Select, Tag, Typography, message } from 'antd'; import { PlusOutlined } from '@ant-design/icons'; import type { ActionType, ProColumns } from '@ant-design/pro-components'; import { ProTable } from '@ant-design/pro-components'; import { getDictValueEnum } from '@/services/system/dict'; import { + getParticipatingCompanyQrCode, getParticipatingCompanies, getParticipatingJobDetail, getParticipatingJobs, removeJobFromCompany, removeParticipatingCompany, + reviewParticipatingCompany, + reviewParticipatingJob, } from '@/services/jobportal/outdoorFairDetail'; import CompanyAddModal from './CompanyAddModal'; import JobEditModal from './JobEditModal'; @@ -18,20 +21,46 @@ interface Props { fairId: number; } +type ReviewTarget = + | { + type: 'company'; + record: API.OutdoorFairDetail.ParticipatingCompany; + } + | { + type: 'job'; + record: API.OutdoorFairDetail.PostedJob; + }; + const ParticipatingCompaniesTab: React.FC = ({ fairId }) => { const companyActionRef = useRef(); const jobActionRef = useRef(); + const [reviewForm] = Form.useForm(); const [companyModalOpen, setCompanyModalOpen] = useState(false); const [jobListModalOpen, setJobListModalOpen] = useState(false); const [jobEditModalOpen, setJobEditModalOpen] = useState(false); const [selectedCompany, setSelectedCompany] = useState(null); + const [qrModalOpen, setQrModalOpen] = useState(false); + const [qrInfo, setQrInfo] = useState(null); + const [qrCompanyName, setQrCompanyName] = useState(''); const [editingJob, setEditingJob] = useState(null); const [educationEnum, setEducationEnum] = useState({}); const [experienceEnum, setExperienceEnum] = useState({}); const [areaEnum, setAreaEnum] = useState({}); const [jobTypeEnum, setJobTypeEnum] = useState({}); const [scaleEnum, setScaleEnum] = useState({}); + const [reviewModalOpen, setReviewModalOpen] = useState(false); + const [reviewTarget, setReviewTarget] = useState(null); + + const renderReviewStatus = (status?: string) => { + const map: Record = { + '0': { color: 'processing', text: '待审核' }, + '1': { color: 'success', text: '已通过' }, + '2': { color: 'error', text: '已驳回' }, + }; + const item = map[status || '1'] || map['1']; + return {item.text}; + }; useEffect(() => { Promise.all([ @@ -63,6 +92,67 @@ const ParticipatingCompaniesTab: React.FC = ({ fairId }) => { setJobEditModalOpen(true); }; + const openQrCodeModal = async (record: API.OutdoorFairDetail.ParticipatingCompany) => { + const res = await getParticipatingCompanyQrCode(fairId, record.companyId); + if (res.code !== 200) { + message.error(res.msg || '二维码内容获取失败'); + return; + } + setQrInfo(res.data); + setQrCompanyName(record.companyName); + setQrModalOpen(true); + }; + + const openReviewModal = (target: ReviewTarget) => { + setReviewTarget(target); + reviewForm.setFieldsValue({ + reviewStatus: + target.type === 'company' + ? target.record.reviewStatus || '0' + : target.record.fairReviewStatus || '0', + reviewRemark: + target.type === 'company' + ? target.record.reviewRemark || '' + : target.record.fairReviewRemark || '', + }); + setReviewModalOpen(true); + }; + + const submitReview = async () => { + const values = await reviewForm.validateFields(); + if (!reviewTarget) return; + const payload = { + reviewStatus: values.reviewStatus as '0' | '1' | '2', + reviewRemark: values.reviewRemark, + }; + const res = + reviewTarget.type === 'company' + ? await reviewParticipatingCompany({ + fairId, + id: reviewTarget.record.id, + ...payload, + }) + : await reviewParticipatingJob({ + fairId, + jobId: reviewTarget.record.jobId!, + ...payload, + }); + if (res.code === 200) { + message.success('审核成功'); + setReviewModalOpen(false); + setReviewTarget(null); + reviewForm.resetFields(); + if (reviewTarget.type === 'company') { + companyActionRef.current?.reload(); + } else { + jobActionRef.current?.reload(); + companyActionRef.current?.reload(); + } + } else { + message.error(res.msg || '审核失败'); + } + }; + const jobColumns: ProColumns[] = [ { title: '岗位名称', dataIndex: 'jobTitle', ellipsis: true }, { @@ -85,6 +175,13 @@ const ParticipatingCompaniesTab: React.FC = ({ fairId }) => { width: 110, }, { title: '招聘人数', dataIndex: 'vacancies', hideInSearch: true, width: 90 }, + { + title: '审核状态', + dataIndex: 'fairReviewStatus', + hideInSearch: true, + width: 100, + render: (_, record) => renderReviewStatus(record.fairReviewStatus), + }, { title: '发布状态', dataIndex: 'isPublish', @@ -99,11 +196,19 @@ const ParticipatingCompaniesTab: React.FC = ({ fairId }) => { { title: '操作', valueType: 'option', - width: 130, + width: 240, render: (_, record) => [ , + , , ]} @@ -180,6 +290,12 @@ const ParticipatingCompaniesTab: React.FC = ({ fairId }) => { }, { title: '联系人', dataIndex: 'contactPerson', hideInSearch: true }, { title: '联系电话', dataIndex: 'contactPhone', hideInSearch: true }, + { + title: '审核状态', + dataIndex: 'reviewStatus', + hideInSearch: true, + render: (_, record) => renderReviewStatus(record.reviewStatus), + }, { title: '展位号', dataIndex: 'boothNumber', @@ -190,7 +306,7 @@ const ParticipatingCompaniesTab: React.FC = ({ fairId }) => { { title: '操作', valueType: 'option', - width: 220, + width: 460, render: (_, record) => [ , + , + , , ]} @@ -276,6 +408,97 @@ const ParticipatingCompaniesTab: React.FC = ({ fairId }) => { )} + qrInfo?.h5Url && window.open(qrInfo.h5Url)} + > + 打开链接 + , + ]} + destroyOnClose + onCancel={() => { + setQrModalOpen(false); + setQrInfo(null); + setQrCompanyName(''); + }} + > + {qrInfo?.qrCodeContent && ( +
+ + + {qrInfo.qrCodeContent} + + {!qrInfo.deviceCode && ( + + 当前企业未绑定设备码,二维码使用企业公共页链接。 + + )} +
+ )} +
+ + { + setReviewModalOpen(false); + setReviewTarget(null); + reviewForm.resetFields(); + }} + onOk={submitReview} + > +
+ + + + prev.reviewStatus !== cur.reviewStatus}> + {({ getFieldValue }) => ( + + + + )} + + +
); }; diff --git a/src/pages/Jobfair/Outdoorfair/index.tsx b/src/pages/Jobfair/Outdoorfair/index.tsx index b0ed86c..dc97463 100644 --- a/src/pages/Jobfair/Outdoorfair/index.tsx +++ b/src/pages/Jobfair/Outdoorfair/index.tsx @@ -1,38 +1,74 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useAccess, history } from '@umijs/max'; -import { Button, message, Modal } from 'antd'; +import { useAccess, history, useModel } from '@umijs/max'; +import { Button, message, Modal, QRCode, Tag, Typography } from 'antd'; import { ActionType, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components'; -import { PlusOutlined, DeleteOutlined, FormOutlined, EyeOutlined } from '@ant-design/icons'; +import { + PlusOutlined, + DeleteOutlined, + FormOutlined, + EyeOutlined, + QrcodeOutlined, + UnorderedListOutlined, +} from '@ant-design/icons'; import { getOutdoorFairList, addOutdoorFair, updateOutdoorFair, deleteOutdoorFair, addOutdoorFairDictOption, + signupOutdoorFair, + getMyOutdoorFairQrCode, } from '@/services/jobportal/outdoorFair'; import type { OutdoorFairItem, OutdoorFairListParams, OutdoorFairForm, OutdoorFairDictForm, + OutdoorFairCompanyQrCodeInfo, } from '@/services/jobportal/outdoorFair'; +import { getParticipatingJobs } from '@/services/jobportal/outdoorFairDetail'; import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict'; import { getVenueInfoList } from '@/services/jobportal/venueInfo'; import EditModal from './components/EditModal'; +import JobEditModal from './Detail/components/JobEditModal'; const OUTDOOR_FAIR_TYPE_DICT = 'outdoor_fair_type'; const OUTDOOR_FAIR_REGION_DICT = 'outdoor_fair_region'; const OutdoorFairList: React.FC = () => { const access = useAccess(); + const { initialState } = useModel('@@initialState'); + const isEnterprise = initialState?.currentUser?.userType === 'view'; const actionRef = useRef(); + const jobActionRef = useRef(); const [modalVisible, setModalVisible] = useState(false); const [currentRow, setCurrentRow] = useState(); + const [selectedFair, setSelectedFair] = useState(null); + const [jobListModalOpen, setJobListModalOpen] = useState(false); + const [jobEditModalOpen, setJobEditModalOpen] = useState(false); + const [qrModalOpen, setQrModalOpen] = useState(false); + const [qrInfo, setQrInfo] = useState(null); + const [rejectReasonModalOpen, setRejectReasonModalOpen] = useState(false); + const [rejectReason, setRejectReason] = useState(''); const [fairTypeValueEnum, setFairTypeValueEnum] = useState>({}); const [fairTypeOptions, setFairTypeOptions] = useState([]); const [regionValueEnum, setRegionValueEnum] = useState>({}); const [regionOptions, setRegionOptions] = useState([]); const [venueOptions, setVenueOptions] = useState([]); + const [educationEnum, setEducationEnum] = useState({}); + const [experienceEnum, setExperienceEnum] = useState({}); + const [areaEnum, setAreaEnum] = useState({}); + const [jobTypeEnum, setJobTypeEnum] = useState({}); + + const renderReviewStatus = (status?: string) => { + const map: Record = { + '0': { color: 'processing', text: '待审核' }, + '1': { color: 'success', text: '已通过' }, + '2': { color: 'error', text: '已驳回' }, + }; + const item = map[status || '0'] || map['0']; + return {item.text}; + }; const refreshDictOptions = useCallback(async () => { const [typeValueEnum, typeOptions, regionEnum, regionSelectOptions] = await Promise.all([ @@ -51,6 +87,20 @@ const OutdoorFairList: React.FC = () => { refreshDictOptions(); }, [refreshDictOptions]); + useEffect(() => { + Promise.all([ + getDictValueEnum('education', true, true), + getDictValueEnum('experience', true, true), + getDictValueEnum('area', true, true), + getDictValueEnum('job_type', true, true), + ]).then(([education, experience, area, jobType]) => { + setEducationEnum(education); + setExperienceEnum(experience); + setAreaEnum(area); + setJobTypeEnum(jobType); + }); + }, []); + const refreshVenueOptions = useCallback(async () => { const res = await getVenueInfoList({ current: 1, pageSize: 999 }); if (res.code === 200) { @@ -66,8 +116,10 @@ const OutdoorFairList: React.FC = () => { }, []); useEffect(() => { - refreshVenueOptions(); - }, [refreshVenueOptions]); + if (!isEnterprise) { + refreshVenueOptions(); + } + }, [isEnterprise, refreshVenueOptions]); const handleCreateDictOption = async (values: OutdoorFairDictForm) => { const res = await addOutdoorFairDictOption(values); @@ -96,6 +148,87 @@ const OutdoorFairList: React.FC = () => { }); }; + const getCurrentCompany = ( + record: OutdoorFairItem, + ): API.OutdoorFairDetail.ParticipatingCompany => ({ + id: 0, + fairId: record.id, + companyId: record.companyId!, + companyName: record.companyName || '本企业', + industry: '', + scale: '', + contactPerson: '', + contactPhone: '', + jobCount: 0, + }); + + const handleSignup = async (record: OutdoorFairItem) => { + const res = await signupOutdoorFair(record.id); + if (res.code === 200) { + message.success('报名成功,等待审核'); + actionRef.current?.reload(); + } else { + message.error(res.msg || '报名失败'); + } + }; + + const openQrCode = async (record: OutdoorFairItem) => { + const res = await getMyOutdoorFairQrCode(record.id); + if (res.code === 200) { + setQrInfo(res.data); + setQrModalOpen(true); + } else { + message.error(res.msg || '二维码加载失败'); + } + }; + + const openJobList = (record: OutdoorFairItem) => { + setSelectedFair(record); + setJobListModalOpen(true); + }; + + const openJobEditor = (record: OutdoorFairItem) => { + setSelectedFair(record); + setJobEditModalOpen(true); + }; + + const openRejectReason = (record: OutdoorFairItem) => { + setRejectReason(record.reviewRemark || '暂无驳回原因'); + setRejectReasonModalOpen(true); + }; + + const jobColumns: ProColumns[] = [ + { title: '岗位名称', dataIndex: 'jobTitle', ellipsis: true }, + { + title: '薪资范围', + hideInSearch: true, + render: (_, record) => `${record.minSalary}-${record.maxSalary} 元/月`, + }, + { + title: '学历要求', + dataIndex: 'education', + valueType: 'select', + valueEnum: educationEnum, + width: 110, + }, + { + title: '工作经验', + dataIndex: 'experience', + valueType: 'select', + valueEnum: experienceEnum, + width: 110, + }, + { title: '招聘人数', dataIndex: 'vacancies', hideInSearch: true, width: 90 }, + { + title: '审核状态', + dataIndex: 'fairReviewStatus', + hideInSearch: true, + width: 100, + render: (_, record) => renderReviewStatus(record.fairReviewStatus), + }, + { title: '工作地点', dataIndex: 'jobLocation', hideInSearch: true, ellipsis: true }, + ]; + const columns: ProColumns[] = [ { title: '招聘会标题', @@ -122,7 +255,7 @@ const OutdoorFairList: React.FC = () => { valueEnum: regionValueEnum, }, { - title: '绑定场地', + title: '场地名称', dataIndex: 'venueName', ellipsis: true, valueType: 'select', @@ -135,18 +268,7 @@ const OutdoorFairList: React.FC = () => { search: { transform: (value) => ({ venueId: value }), }, - render: (_, record) => - record.venueId && record.venueName ? ( - - ) : ( - '--' - ), + render: (_, record) => record.venueName || '--', }, { title: '举办地址', @@ -188,11 +310,29 @@ const OutdoorFairList: React.FC = () => { dataIndex: 'onlineApply', valueType: 'select', valueEnum: { - 'true': { text: '是' }, - 'false': { text: '否' }, + true: { text: '是' }, + false: { text: '否' }, }, render: (_, record) => (record.onlineApply ? '是' : '否'), }, + { + title: '报名审核状态', + dataIndex: 'reviewStatus', + hideInSearch: true, + hideInTable: !isEnterprise, + render: (_, record) => + record.signedUp ? ( + record.reviewStatus === '2' ? ( + + ) : ( + renderReviewStatus(record.reviewStatus) + ) + ) : ( + '--' + ), + }, { title: '招聘会照片', dataIndex: 'photoUrl', @@ -211,14 +351,47 @@ const OutdoorFairList: React.FC = () => { { title: '操作', valueType: 'option', - width: 160, + width: isEnterprise ? 280 : 160, fixed: 'right', render: (_, record) => [ + isEnterprise && !record.signedUp ? ( + + ) : null, + isEnterprise && record.signedUp && record.reviewStatus === '1' ? ( + + ) : null, + isEnterprise && record.signedUp && record.reviewStatus === '1' ? ( + + ) : null, + isEnterprise && record.signedUp && record.reviewStatus === '1' ? ( + + ) : null, , + ]} + destroyOnClose + onCancel={() => { + setQrModalOpen(false); + setQrInfo(null); + }} + > + {qrInfo?.qrCodeContent && ( +
+ + + {qrInfo.qrCodeContent} + +
+ )} + + { + setRejectReasonModalOpen(false); + setRejectReason(''); + }} + > + + {rejectReason} + + + { + setJobListModalOpen(false); + setSelectedFair(null); + }} + > + {selectedFair && ( + + actionRef={jobActionRef} + rowKey="jobId" + options={false} + search={{ labelWidth: 'auto', defaultCollapsed: false }} + pagination={{ pageSize: 10 }} + toolBarRender={() => [ + , + ]} + request={async (params) => { + const res = await getParticipatingJobs(selectedFair.id, { + current: params.current, + pageSize: params.pageSize, + jobTitle: params.jobTitle, + education: params.education, + experience: params.experience, + }); + return { data: res.rows || [], total: res.total || 0, success: res.code === 200 }; + }} + columns={jobColumns} + /> + )} + + setJobEditModalOpen(false)} + onSuccess={() => { + setJobEditModalOpen(false); + jobActionRef.current?.reload(); + actionRef.current?.reload(); + }} + /> ); }; diff --git a/src/pages/Jobfair/PublicJobFair/Detail/index.tsx b/src/pages/Jobfair/PublicJobFair/Detail/index.tsx index 587c190..82f9578 100644 --- a/src/pages/Jobfair/PublicJobFair/Detail/index.tsx +++ b/src/pages/Jobfair/PublicJobFair/Detail/index.tsx @@ -102,8 +102,11 @@ const JobFairDetail: React.FC = () => { {detail?.jobFairSignUpStartTime} ~ {detail?.jobFairSignUpEndTime} {detail?.jobFairHostUnit} - {detail?.jobFairCoOrganizer} - {detail?.jobFairOrganizer} + {detail?.jobFairHelpUnit} + {detail?.jobFairOrganizeUnit} + + {detail?.crossDomainCities || '-'} + {detail?.jobFairIntroduction} diff --git a/src/pages/Jobfair/PublicJobFair/components/EditModal.tsx b/src/pages/Jobfair/PublicJobFair/components/EditModal.tsx index 476a6f2..2393a93 100644 --- a/src/pages/Jobfair/PublicJobFair/components/EditModal.tsx +++ b/src/pages/Jobfair/PublicJobFair/components/EditModal.tsx @@ -6,9 +6,13 @@ import { ProFormTextArea, ProFormRadio, ProFormDateTimePicker, + ProFormList, + ProFormSelect, } from '@ant-design/pro-components'; import { Button, Form } from 'antd'; import ProFromMap from '@/components/ProFromMap'; +import { getDictValueEnum } from '@/services/system/dict'; +import { getCrossCityAllianceList } from '@/services/jobfair/crossCityAlliance'; interface EditModalProps { open: boolean; @@ -18,22 +22,54 @@ interface EditModalProps { onSubmit: (values: API.PublicJobFair.JobFairForm) => Promise; } +/** 逗号串 -> 数组(空值返回 [''] 保证至少一行) */ +const toArr = (s?: string): string[] => { + if (!s) return ['']; + const arr = s.split(',').map((x) => x.trim()).filter(Boolean); + return arr.length ? arr : ['']; +}; + +/** 数组 -> 逗号串(过滤空串) */ +const toStr = (arr?: string[]): string => + (arr || []).map((x) => (x || '').trim()).filter(Boolean).join(','); + const EditModal: React.FC = ({ open, values, jobFairTypeEnum, onCancel, onSubmit }) => { const [form] = Form.useForm(); const [mapOpen, setMapOpen] = useState(false); const [locationInfo, setLocationInfo] = useState<{ address?: string; latitude?: number; longitude?: number }>({}); + const [yesNoEnum, setYesNoEnum] = useState({}); + + const isCrossDomain = Form.useWatch('isCrossDomain', form); + + useEffect(() => { + getDictValueEnum('sys_yes_no').then(setYesNoEnum); + }, []); useEffect(() => { if (open) { form.resetFields(); if (values) { - form.setFieldsValue(values); + form.setFieldsValue({ + ...values, + jobFairHostUnitList: toArr(values.jobFairHostUnit), + jobFairHelpUnitList: toArr(values.jobFairHelpUnit), + jobFairOrganizeUnitList: toArr(values.jobFairOrganizeUnit), + crossDomainCityList: values.crossDomainCities + ? values.crossDomainCities.split(',').map((x) => x.trim()).filter(Boolean) + : [], + }); setLocationInfo({ address: values.jobFairAddress, latitude: values.latitude, longitude: values.longitude, }); } else { + form.setFieldsValue({ + jobFairHostUnitList: [''], + jobFairHelpUnitList: [''], + jobFairOrganizeUnitList: [''], + crossDomainCityList: [], + }); setLocationInfo({}); } } @@ -59,8 +95,26 @@ const EditModal: React.FC = ({ open, values, jobFairTypeEnum, on open={open} width={700} modalProps={{ destroyOnClose: true, onCancel }} + initialValues={{ isCrossDomain: 'n' }} onFinish={async (formValues) => { - await onSubmit({ ...formValues, jobFairId: values?.jobFairId } as API.PublicJobFair.JobFairForm); + const v = formValues as any; + const { + jobFairHostUnitList, + jobFairHelpUnitList, + jobFairOrganizeUnitList, + crossDomainCityList, + ...rest + } = v; + const payload: API.PublicJobFair.JobFairForm = { + ...rest, + jobFairId: values?.jobFairId, + jobFairHostUnit: toStr(jobFairHostUnitList), + jobFairHelpUnit: toStr(jobFairHelpUnitList), + jobFairOrganizeUnit: toStr(jobFairOrganizeUnitList), + crossDomainCities: + isCrossDomain === 'y' ? toStr(crossDomainCityList) : '', + }; + await onSubmit(payload); }} >