diff --git a/config/config.ts b/config/config.ts index 4fd455f..a47d7dd 100644 --- a/config/config.ts +++ b/config/config.ts @@ -150,6 +150,9 @@ export default defineConfig({ ], mfsu: { strategy: 'normal', + // react-rnd 由招聘会展位图按需加载。避免旧的 MFSU 容器缺少该模块时, + // 详情页运行时报 “Module ./react-rnd does not exist in container”。 + exclude: ['react-rnd'], }, outputPath: 'shihezi', base: '/shihezi/', diff --git a/config/routes.ts b/config/routes.ts index 1f28bf6..4c99e91 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -236,6 +236,11 @@ export default [ path: '/jobfair/venue-info', component: './Jobfair/Venueinfo', }, + { + name: '场地信息详情', + path: '/jobfair/venue-info/detail', + component: './Jobfair/Venueinfo/Detail', + }, { name: '户外招聘会详情', path: '/jobfair/outdoor-fair/detail', diff --git a/src/pages/Jobfair/Outdoorfair/Detail/components/AttendeeTab.tsx b/src/pages/Jobfair/Outdoorfair/Detail/components/AttendeeTab.tsx deleted file mode 100644 index ad55de1..0000000 --- a/src/pages/Jobfair/Outdoorfair/Detail/components/AttendeeTab.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import React, { useRef, useState } from 'react'; -import { useAccess } from '@umijs/max'; -import { Button, message, Modal, Tag, Drawer, Space } from 'antd'; -import { ActionType, ProColumns, ProTable, ModalForm, ProFormText, ProFormSelect, ProFormDateTimePicker, ProFormTextArea } from '@ant-design/pro-components'; -import { ScanOutlined, EditOutlined, ScheduleOutlined, ExportOutlined } from '@ant-design/icons'; -import { - getAttendeeList, - scanAttendeeEntry, - updateAttendeeResume, - trackInterview, -} from '@/services/jobportal/outdoorFairDetail'; - -interface Props { - fairId: number; -} - -const checkInMethodMap: Record = { - qr_scan: '扫码入场', id_card: '刷身份证', manual: '手动登记', -}; - -const interviewStatusMap: Record = { - none: { text: '未面试', color: 'default' }, - scheduled: { text: '已预约', color: 'blue' }, - in_progress: { text: '面试中', color: 'processing' }, - completed: { text: '已完成', color: 'green' }, - passed: { text: '已通过', color: 'green' }, - failed: { text: '未通过', color: 'red' }, -}; - -const AttendeeTab: React.FC = ({ fairId }) => { - const access = useAccess(); - const actionRef = useRef(); - const [scanModalOpen, setScanModalOpen] = useState(false); - const [resumeDrawerOpen, setResumeDrawerOpen] = useState(false); - const [currentAttendee, setCurrentAttendee] = useState(null); - const [interviewModalOpen, setInterviewModalOpen] = useState(false); - - const columns: ProColumns[] = [ - { - title: '姓名', - dataIndex: 'name', - width: 100, - hideInTable: true, - fieldProps: { placeholder: '请输入姓名' }, - }, - { - title: '手机号', - dataIndex: 'phone', - width: 130, - hideInTable: true, - fieldProps: { placeholder: '请输入手机号' }, - }, - { - title: '面试状态', - dataIndex: 'interviewStatus', - hideInTable: true, - valueType: 'select', - options: [ - { label: '未面试', value: 'none' }, - { label: '已预约', value: 'scheduled' }, - { label: '面试中', value: 'in_progress' }, - { label: '已完成', value: 'completed' }, - { label: '已通过', value: 'passed' }, - { label: '未通过', value: 'failed' }, - ], - }, - { title: '姓名', dataIndex: 'name', width: 100, hideInSearch: true }, - { title: '性别', dataIndex: 'gender', width: 60, hideInSearch: true }, - { title: '手机号', dataIndex: 'phone', width: 130, hideInSearch: true }, - { title: '身份证号', dataIndex: 'idCard', width: 180, hideInSearch: true }, - { title: '学历', dataIndex: 'education', width: 80, hideInSearch: true }, - { title: '毕业院校', dataIndex: 'graduationSchool', width: 150, hideInSearch: true, ellipsis: true }, - { title: '入场时间', dataIndex: 'checkInTime', valueType: 'dateTime', width: 170, hideInSearch: true, render: (_, r) => r.checkInTime || '未入场' }, - { title: '入场方式', dataIndex: 'checkInMethod', width: 100, hideInSearch: true, - render: (_, r) => checkInMethodMap[r.checkInMethod] || r.checkInMethod }, - { title: '意向企业', dataIndex: 'targetCompanies', width: 180, ellipsis: true, hideInSearch: true }, - { title: '面试状态', dataIndex: 'interviewStatus', width: 100, hideInSearch: true, - render: (_, r) => { - const s = interviewStatusMap[r.interviewStatus] || { text: r.interviewStatus, color: 'default' }; - return {s.text}; - }, - }, - { - title: '操作', valueType: 'option', width: 220, - render: (_, record) => [ - , - , - , - ], - }, - ]; - - return ( -
- { - const res = await getAttendeeList(fairId, { - current: params.current, pageSize: params.pageSize, - name: params.name, phone: params.phone, interviewStatus: params.interviewStatus, - }); - return { data: res.rows, total: res.total, success: true }; - }} - toolBarRender={() => [ - , - , - ]} - /> - - {/* 扫码入场弹窗 */} - setScanModalOpen(false) }} - onFinish={async (values) => { - const res = await scanAttendeeEntry(values); - if (res.code === 200) { message.success(res.msg); setScanModalOpen(false); actionRef.current?.reload(); return true; } - message.error(res.msg); return false; - }} - > - - - - - - - {/* 简历编辑抽屉 */} - setResumeDrawerOpen(false)} - destroyOnClose - > - { - if (!currentAttendee) return; - const res = await updateAttendeeResume(currentAttendee.id, values); - if (res.code === 200) { message.success('简历更新成功'); setResumeDrawerOpen(false); actionRef.current?.reload(); return true; } - message.error(res.msg); return false; - }} - > - ({ label: v, value: v }))} - /> - - - - - - - {/* 面试跟踪弹窗 */} - setInterviewModalOpen(false) }} - initialValues={currentAttendee} - onFinish={async (values) => { - if (!currentAttendee) return; - const res = await trackInterview({ attendeeId: currentAttendee.id, ...values }); - if (res.code === 200) { message.success('面试信息更新成功'); setInterviewModalOpen(false); actionRef.current?.reload(); return true; } - message.error(res.msg); return false; - }} - > - - - - - -
- ); -}; - -export default AttendeeTab; diff --git a/src/pages/Jobfair/Outdoorfair/Detail/components/CompanyReviewTab.tsx b/src/pages/Jobfair/Outdoorfair/Detail/components/CompanyReviewTab.tsx deleted file mode 100644 index eba4a8d..0000000 --- a/src/pages/Jobfair/Outdoorfair/Detail/components/CompanyReviewTab.tsx +++ /dev/null @@ -1,196 +0,0 @@ -import React, { useRef, useState } from 'react'; -import { useAccess } from '@umijs/max'; -import { Button, message, Modal, Tag, Space } from 'antd'; -import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components'; -import { CheckOutlined, CloseOutlined, QrcodeOutlined, SendOutlined } from '@ant-design/icons'; -import { - getCompanyRegistrationList, - reviewCompanyRegistration, - reviewRegisteredJob, - qrCheckIn, -} from '@/services/jobportal/outdoorFairDetail'; - -interface Props { - fairId: number; -} - -const reviewStatusMap: Record = { - pending: { text: '待审核', color: 'default' }, - approved: { text: '已通过', color: 'green' }, - rejected: { text: '已拒绝', color: 'red' }, -}; - -const CompanyReviewTab: React.FC = ({ fairId }) => { - const access = useAccess(); - const actionRef = useRef(); - const [rejectModalOpen, setRejectModalOpen] = useState(false); - const [rejectTarget, setRejectTarget] = useState(null); - - const handleReviewCompany = async (id: number, status: string) => { - if (status === 'rejected') { - Modal.confirm({ - title: '确认拒绝', content: '确定拒绝该单位的参会申请吗?', - onOk: async () => { - const res = await reviewCompanyRegistration(id, status); - if (res.code === 200) { message.success(res.msg); actionRef.current?.reload(); } - }, - }); - } else { - const res = await reviewCompanyRegistration(id, status); - if (res.code === 200) { message.success(res.msg); actionRef.current?.reload(); } - } - }; - - const handleReviewJob = async (regId: number, jobId: number, status: string) => { - const res = await reviewRegisteredJob(regId, jobId, status); - if (res.code === 200) { message.success(res.msg); actionRef.current?.reload(); } - }; - - const handleQrCheckIn = async (id: number) => { - Modal.confirm({ - title: '扫码签到', - content: '确认该单位已完成现场扫码签到吗?', - onOk: async () => { - const res = await qrCheckIn(id); - if (res.code === 200) { message.success(res.msg); actionRef.current?.reload(); } - }, - }); - }; - - const handleNotify = (companyName: string, method: string) => { - message.success(`已通过${method}向「${companyName}」发送审核结果通知`); - }; - - const columns: ProColumns[] = [ - { - title: '企业名称', - dataIndex: 'companyName', - ellipsis: true, - hideInTable: true, - fieldProps: { placeholder: '请输入企业名称' }, - }, - { - title: '审核状态', - dataIndex: 'reviewStatus', - hideInTable: true, - valueType: 'select', - options: [ - { label: '待审核', value: 'pending' }, - { label: '已通过', value: 'approved' }, - { label: '已拒绝', value: 'rejected' }, - ], - }, - { title: '企业名称', dataIndex: 'companyName', ellipsis: true, hideInSearch: true }, - { title: '行业', dataIndex: 'industry', width: 80, hideInSearch: true, render: (_, r) => {r.industry} }, - { title: '规模', dataIndex: 'scale', width: 100, hideInSearch: true }, - { title: '联系人', dataIndex: 'contactPerson', width: 100, hideInSearch: true }, - { title: '联系电话', dataIndex: 'contactPhone', width: 130, hideInSearch: true }, - { title: '审核状态', dataIndex: 'reviewStatus', width: 100, hideInSearch: true, - render: (_, r) => { - const s = reviewStatusMap[r.reviewStatus] || { text: r.reviewStatus, color: 'default' }; - return {s.text}; - }, - }, - { title: '签到状态', dataIndex: 'qrCheckInStatus', width: 100, hideInSearch: true, - render: (_, r) => - {r.qrCheckInStatus === 'checked_in' ? '已签到' : '未签到'} - , - }, - { title: '签到时间', dataIndex: 'checkInTime', valueType: 'dateTime', width: 170, hideInSearch: true, render: (_, r) => r.checkInTime || '--' }, - { - title: '操作', valueType: 'option', width: 300, - render: (_, record) => [ - record.reviewStatus === 'pending' && ( - - ), - record.reviewStatus === 'pending' && ( - - ), - record.reviewStatus === 'approved' && ( - <> - - - {record.qrCheckInStatus !== 'checked_in' && ( - - )} - - ), - ], - }, - ]; - - // 展开行:岗位审核 - const expandedRowRender = (record: any) => { - if (!record.registeredJobs || record.registeredJobs.length === 0) { - return
该单位未发布岗位
; - } - return ( - `${r.minSalary}K-${r.maxSalary}K` }, - { title: '学历要求', dataIndex: 'education', width: 80 }, - { title: '招聘人数', dataIndex: 'vacancies', width: 80 }, - { title: '审核状态', dataIndex: 'reviewStatus', width: 100, - render: (_, r: any) => { - const s = reviewStatusMap[r.reviewStatus] || { text: r.reviewStatus, color: 'default' }; - return {s.text}; - }, - }, - { - title: '操作', valueType: 'option', width: 150, - render: (_, jobRecord: any) => [ - jobRecord.reviewStatus === 'pending' && ( - - ), - jobRecord.reviewStatus === 'pending' && ( - - ), - ], - }, - ]} - /> - ); - }; - - return ( - { - const res = await getCompanyRegistrationList(fairId, { - current: params.current, pageSize: params.pageSize, - companyName: params.companyName, reviewStatus: params.reviewStatus, - }); - return { data: res.rows, total: res.total, success: true }; - }} - /> - ); -}; - -export default CompanyReviewTab; diff --git a/src/pages/Jobfair/Outdoorfair/Detail/components/DeviceTab.tsx b/src/pages/Jobfair/Outdoorfair/Detail/components/DeviceTab.tsx deleted file mode 100644 index 8252dc6..0000000 --- a/src/pages/Jobfair/Outdoorfair/Detail/components/DeviceTab.tsx +++ /dev/null @@ -1,291 +0,0 @@ -import React, { useRef, useState, useEffect } from 'react'; -import { useAccess } from '@umijs/max'; -import { Button, message, Modal, Tag, Row, Col, Card, Switch, Statistic, Badge, Dropdown, Space } from 'antd'; -import { ActionType, ProColumns, ProTable, ModalForm, ProFormText, ProFormSelect } from '@ant-design/pro-components'; -import { PlusOutlined, DeleteOutlined, FormOutlined, QrcodeOutlined, SwapOutlined, ApartmentOutlined, ScanOutlined, EllipsisOutlined } from '@ant-design/icons'; -import { - getDeviceList, - addDevice, - updateDevice, - deleteDevice, - assignDevice, - toggleDeviceStatus, - getGateStatuses, - toggleGate, - getGateEntryLogs, - simulateGateScan, -} from '@/services/jobportal/outdoorFairDetail'; -import { getBoothList } from '@/services/jobportal/outdoorFairDetail'; - -interface Props { - fairId: number; -} - -const deviceTypeMap: Record = { - mobile_terminal: '移动终端', qr_sticker: '展位二维码', -}; - -const DeviceTab: React.FC = ({ fairId }) => { - const access = useAccess(); - const mobileActionRef = useRef(); - const qrActionRef = useRef(); - const gateLogActionRef = useRef(); - const [deviceModalOpen, setDeviceModalOpen] = useState(false); - const [editingDevice, setEditingDevice] = useState(null); - const [defaultType, setDefaultType] = useState('mobile_terminal'); - const [assignModalOpen, setAssignModalOpen] = useState(false); - const [assigningDevice, setAssigningDevice] = useState(null); - const [gateStatuses, setGateStatuses] = useState([]); - const [scanModalOpen, setScanModalOpen] = useState(false); - const [scanGate, setScanGate] = useState(null); - - const fetchGateStatuses = async () => { - const res = await getGateStatuses(fairId); - setGateStatuses(res.rows || []); - }; - - useEffect(() => { - fetchGateStatuses(); - }, [fairId]); - - const handleToggleGate = async (id: number) => { - const res = await toggleGate(id); - if (res.code === 200) { message.success(res.msg); fetchGateStatuses(); } - }; - - const deviceColumns = (type: string): ProColumns[] => [ - { title: '设备名称', dataIndex: 'deviceName', width: 120, ellipsis: true }, - { title: '设备编号', dataIndex: 'deviceCode', width: 180, hideInSearch: true }, - { title: '分配展位', dataIndex: 'assignedBoothNumber', width: 100, hideInSearch: true, render: (_, r) => r.assignedBoothNumber || '--' }, - { title: '分配企业', dataIndex: 'assignedCompanyName', width: 180, ellipsis: true, hideInSearch: true, render: (_, r) => r.assignedCompanyName || '--' }, - { title: '状态', dataIndex: 'status', width: 80, hideInSearch: true, - render: (_, r) => , - }, - { title: '最后活跃', dataIndex: 'lastActiveTime', width: 170, hideInSearch: true, render: (_, r) => r.lastActiveTime || '--' }, - { - title: '操作', valueType: 'option', width: 120, - render: (_, record) => { - const isQrType = type === 'qr_sticker'; - const menuItems = [ - { - key: 'toggle', - icon: record.status === 'online' ? : , - label: record.status === 'online' ? '禁用' : '启用', - onClick: async () => { - const res = await toggleDeviceStatus(record.id); - if (res.code === 200) { message.success(res.msg); type === 'mobile_terminal' ? mobileActionRef.current?.reload() : qrActionRef.current?.reload(); } - }, - }, - { - key: 'assign', - icon: , - label: '分配展位', - onClick: () => { setAssigningDevice(record); setAssignModalOpen(true); }, - }, - ...(isQrType ? [{ - key: 'qrcode', - icon: , - label: '重新生成二维码', - onClick: () => message.success(`已为「${record.deviceName}」重新生成二维码`), - }] : []), - { - key: 'edit', - icon: , - label: '编辑', - onClick: () => { setEditingDevice(record); setDefaultType(record.type); setDeviceModalOpen(true); }, - }, - { type: 'divider' as const }, - { - key: 'delete', - icon: , - label: '删除', - danger: true, - onClick: () => { - Modal.confirm({ - title: '确认删除', content: `确定删除「${record.deviceName}」吗?`, - onOk: async () => { - const res = await deleteDevice(record.id); - if (res.code === 200) { message.success('删除成功'); type === 'mobile_terminal' ? mobileActionRef.current?.reload() : qrActionRef.current?.reload(); } - }, - }); - }, - }, - ]; - return ( - - - - ); - }, - }, - ]; - - return ( -
- - - } - onClick={() => { setEditingDevice(null); setDefaultType('mobile_terminal'); setDeviceModalOpen(true); }} - >新增 - } - bodyStyle={{ padding: 0 }} - > - { - const res = await getDeviceList(fairId, { current: params.current, pageSize: params.pageSize, type: 'mobile_terminal' }); - return { data: res.rows, total: res.total, success: true }; - }} - /> - - - - } - onClick={() => { setEditingDevice(null); setDefaultType('qr_sticker'); setDeviceModalOpen(true); }} - >新增 - } - bodyStyle={{ padding: 0 }} - > - { - const res = await getDeviceList(fairId, { current: params.current, pageSize: params.pageSize, type: 'qr_sticker' }); - return { data: res.rows, total: res.total, success: true }; - }} - /> - - - - - {/* 闸机管理 */} - - - {gateStatuses.map((gate: any) => ( - - - - - {gate.gateName} - handleToggleGate(gate.id)} - checkedChildren="开" unCheckedChildren="关" /> - {gate.isOpen ? '已开启' : '已关闭'} - - - - - - - ))} - - r.scanMethod === 'qr' ? '二维码' : '身份证' }, - ]} - request={async (params) => { - const res = await getGateEntryLogs(fairId, { current: params.current, pageSize: params.pageSize }); - return { data: res.rows, total: res.total, success: true }; - }} - /> - - - {/* 新增/编辑设备 */} - setDeviceModalOpen(false) }} - initialValues={editingDevice || { type: defaultType }} - onFinish={async (values) => { - const res = editingDevice - ? await updateDevice({ id: editingDevice.id, ...values }) - : await addDevice({ fairId, ...values }); - if (res.code === 200) { message.success(res.msg); setDeviceModalOpen(false); mobileActionRef.current?.reload(); qrActionRef.current?.reload(); return true; } - message.error(res.msg); return false; - }} - > - - - - - - {/* 分配展位 */} - setAssignModalOpen(false) }} - onFinish={async (values) => { - if (!assigningDevice) return; - const booth = (await getBoothList(fairId, { current: 1, pageSize: 100 })).rows.find((b: any) => b.id === values.boothId); - const res = await assignDevice(assigningDevice.id, values.boothId, booth?.boothNumber || '', booth?.companyName || ''); - if (res.code === 200) { message.success(res.msg); setAssignModalOpen(false); mobileActionRef.current?.reload(); qrActionRef.current?.reload(); return true; } - message.error(res.msg); return false; - }} - > - { - const res = await getBoothList(fairId); - return res.rows.map((b: any) => ({ label: `${b.boothNumber} (${b.companyName || '未分配'})`, value: b.id })); - }} - placeholder="请搜索并选择展位" - /> - - - {/* 闸机扫码 */} - setScanModalOpen(false) }} - onFinish={async (values) => { - if (!scanGate) return; - const res = await simulateGateScan({ fairId, gateName: scanGate.gateName, ...values }); - if (res.code === 200) { message.success(res.msg); setScanModalOpen(false); gateLogActionRef.current?.reload(); fetchGateStatuses(); return true; } - message.error(res.msg); return false; - }} - > - - - -
- ); -}; - -export default DeviceTab; diff --git a/src/pages/Jobfair/Outdoorfair/Detail/components/FairInfoTab.tsx b/src/pages/Jobfair/Outdoorfair/Detail/components/FairInfoTab.tsx index 9c4f449..534d2a5 100644 --- a/src/pages/Jobfair/Outdoorfair/Detail/components/FairInfoTab.tsx +++ b/src/pages/Jobfair/Outdoorfair/Detail/components/FairInfoTab.tsx @@ -1,54 +1,43 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { history, useAccess } from '@umijs/max'; -import { Button, Card, Descriptions, List, Tag, message, Modal, Space } from 'antd'; -import { ActionType, ProTable } from '@ant-design/pro-components'; -import { PlusOutlined, DeleteOutlined, FormOutlined } from '@ant-design/icons'; +import { Button, Card, Descriptions, Image, Tag, message } from 'antd'; +import { FormOutlined } from '@ant-design/icons'; import type { OutdoorFairDictForm, OutdoorFairItem } from '@/services/jobportal/outdoorFair'; import { addOutdoorFairDictOption, updateOutdoorFair } from '@/services/jobportal/outdoorFair'; -import { getVenueInfoList } from '@/services/jobportal/venueInfo'; -import { getDictSelectOption } from '@/services/system/dict'; -import { - getParticipatingCompanies, - addParticipatingCompany, - removeParticipatingCompany, - addJobForCompany, - updateJobForCompany, - removeJobFromCompany, -} from '@/services/jobportal/outdoorFairDetail'; +import { getVenueInfo, getVenueInfoList } from '@/services/jobportal/venueInfo'; +import type { VenueInfoItem } from '@/services/jobportal/venueInfo'; +import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict'; import EditModal from '@/pages/Jobfair/Outdoorfair/components/EditModal'; -import CompanyAddModal from './CompanyAddModal'; -import JobEditModal from './JobEditModal'; const OUTDOOR_FAIR_TYPE_DICT = 'outdoor_fair_type'; const OUTDOOR_FAIR_REGION_DICT = 'outdoor_fair_region'; +const VENUE_INFO_TYPE_DICT = 'venue_info_type'; interface Props { fairId: number; fairInfo: OutdoorFairItem; - refreshKey?: number; onFairUpdate: () => void; } -const FairInfoTab: React.FC = ({ fairId, fairInfo, refreshKey, onFairUpdate }) => { +const FairInfoTab: React.FC = ({ fairInfo, onFairUpdate }) => { const access = useAccess(); - const actionRef = useRef(); const [editModalOpen, setEditModalOpen] = useState(false); - const [companyModalOpen, setCompanyModalOpen] = useState(false); - const [jobModalOpen, setJobModalOpen] = useState(false); - const [selectedCompany, setSelectedCompany] = useState(null); - const [editingJob, setEditingJob] = useState(null); const [fairTypeOptions, setFairTypeOptions] = useState([]); const [regionOptions, setRegionOptions] = useState([]); const [venueOptions, setVenueOptions] = useState([]); + const [venueDetail, setVenueDetail] = useState(); + const [venueTypeValueEnum, setVenueTypeValueEnum] = useState>({}); const refreshEditModalOptions = useCallback(async () => { - const [typeOptions, regionSelectOptions, venueRes] = await Promise.all([ + const [typeOptions, regionSelectOptions, venueRes, venueTypeValueEnum] = await Promise.all([ getDictSelectOption(OUTDOOR_FAIR_TYPE_DICT), getDictSelectOption(OUTDOOR_FAIR_REGION_DICT), getVenueInfoList({ current: 1, pageSize: 999 }), + getDictValueEnum(VENUE_INFO_TYPE_DICT), ]); setFairTypeOptions(typeOptions); setRegionOptions(regionSelectOptions); + setVenueTypeValueEnum(venueTypeValueEnum); if (venueRes.code === 200) { setVenueOptions( (venueRes.rows || []).map((item) => ({ @@ -65,9 +54,26 @@ const FairInfoTab: React.FC = ({ fairId, fairInfo, refreshKey, onFairUpda refreshEditModalOptions(); }, [refreshEditModalOptions]); + // 拉取绑定场地的详细信息 useEffect(() => { - actionRef.current?.reload(); - }, [refreshKey]); + const fetchVenueDetail = async () => { + if (!fairInfo.venueId) { + setVenueDetail(undefined); + return; + } + try { + const res = await getVenueInfo(fairInfo.venueId); + if (res.code === 200 && res.data) { + setVenueDetail(res.data); + } else { + setVenueDetail(undefined); + } + } catch { + setVenueDetail(undefined); + } + }; + fetchVenueDetail(); + }, [fairInfo.venueId]); const handleCreateDictOption = async (values: OutdoorFairDictForm) => { const res = await addOutdoorFairDictOption(values); @@ -91,8 +97,7 @@ const FairInfoTab: React.FC = ({ fairId, fairInfo, refreshKey, onFairUpda }; return ( -
- {/* 基本信息卡片 */} + <> = ({ fairId, fairInfo, refreshKey, onFairUpda 编辑信息 } - style={{ marginBottom: 16 }} > @@ -141,171 +145,66 @@ const FairInfoTab: React.FC = ({ fairId, fairInfo, refreshKey, onFairUpda {fairInfo.onlineApply ? '是' : '否'} + + {fairInfo.photoUrl ? ( + {fairInfo.title} + ) : ( + '--' + )} + - {/* 参会企业及岗位 */} - } onClick={() => setCompanyModalOpen(true)}> - 添加企业 - - } - > - { - const res = await getParticipatingCompanies(fairId, { - current: params.current, - pageSize: params.pageSize, - companyName: params.companyName, - }); - return { data: res.rows, total: res.total, success: true }; - }} - expandable={{ - expandedRowRender: (record) => ( -
- {record.jobList.length === 0 ? ( -

暂无岗位

- ) : ( - ( - { - setEditingJob(job); - setSelectedCompany(record); - setJobModalOpen(true); - }} - > - 编辑 - , - , - ]} - > - - - 薪资 {job.minSalary}-{job.maxSalary}K - - {job.education} - {job.experience} - 招聘 {job.vacancies} 人 - - {job.status === 'active' ? '在招' : '已关闭'} - - - } - /> - - )} - /> - )} -
- ), - }} - columns={[ - { title: '企业名称', dataIndex: 'companyName', ellipsis: true }, - { - title: '行业', - dataIndex: 'industry', - width: 100, - hideInSearch: true, - render: (_, r: any) => {r.industry}, - }, - { title: '规模', dataIndex: 'scale', width: 110, hideInSearch: true }, - { title: '联系人', dataIndex: 'contactPerson', width: 100, hideInSearch: true }, - { title: '联系电话', dataIndex: 'contactPhone', width: 130, hideInSearch: true }, - { - title: '展位号', - dataIndex: 'boothNumber', - width: 80, - hideInSearch: true, - render: (_, r: any) => r.boothNumber || '--', - }, - { - title: '岗位数', - width: 80, - hideInSearch: true, - render: (_, r: any) => r.jobList?.length || 0, - }, - { - title: '操作', - valueType: 'option', - width: 200, - render: (_, record: any) => [ - , - , - ], - }, - ]} - /> + + {venueDetail ? ( + + + {venueDetail.venueName || '--'} + + + {venueTypeValueEnum[venueDetail.venueType]?.text || + venueTypeValueEnum[venueDetail.venueType]?.label || + venueDetail.venueType || + '--'} + + + {venueDetail.venueArea != null ? `${venueDetail.venueArea} ㎡` : '--'} + + + {venueDetail.floorCount ?? '--'} + + + {venueDetail.venueAddress || '--'} + + + {venueDetail.contactPhone || '--'} + + + {venueDetail.routeLineNames || '--'} + + + {venueDetail.handleDate || '--'} + + + {venueDetail.handleOrg || '--'} + + + {venueDetail.handler || '--'} + + + {venueDetail.remark || '--'} + + + ) : ( +
该招聘会未绑定场地信息
+ )}
- {/* 编辑招聘会基本信息弹窗 */} = ({ fairId, fairInfo, refreshKey, onFairUpda } }} /> - - {/* 添加企业弹窗 */} - setCompanyModalOpen(false)} - onSuccess={() => { - setCompanyModalOpen(false); - actionRef.current?.reload(); - }} - /> - - {/* 添加/编辑岗位弹窗 */} - { - setJobModalOpen(false); - setEditingJob(null); - setSelectedCompany(null); - }} - onSuccess={() => { - setJobModalOpen(false); - setEditingJob(null); - actionRef.current?.reload(); - }} - /> -
+ ); }; diff --git a/src/pages/Jobfair/Outdoorfair/Detail/components/JobEditModal.tsx b/src/pages/Jobfair/Outdoorfair/Detail/components/JobEditModal.tsx index ad8aecc..6e1a52d 100644 --- a/src/pages/Jobfair/Outdoorfair/Detail/components/JobEditModal.tsx +++ b/src/pages/Jobfair/Outdoorfair/Detail/components/JobEditModal.tsx @@ -1,26 +1,54 @@ import React, { useEffect } from 'react'; -import { ModalForm, ProForm, ProFormText, ProFormDigit, ProFormSelect } from '@ant-design/pro-components'; -import { Form } from 'antd'; +import { + ModalForm, + ProForm, + ProFormDigit, + ProFormSelect, + ProFormText, + ProFormTextArea, +} from '@ant-design/pro-components'; +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'; interface Props { open: boolean; fairId: number; - company: any; - job: any; + company: API.OutdoorFairDetail.ParticipatingCompany | null; + job: API.OutdoorFairDetail.PostedJob | null; + educationEnum: DictValueEnumObj; + experienceEnum: DictValueEnumObj; + areaEnum: DictValueEnumObj; + jobTypeEnum: DictValueEnumObj; onCancel: () => void; onSuccess: () => void; } -const JobEditModal: React.FC = ({ open, fairId, company, job, onCancel, onSuccess }) => { +const JobEditModal: React.FC = ({ + open, + fairId, + company, + job, + educationEnum, + experienceEnum, + areaEnum, + jobTypeEnum, + onCancel, + onSuccess, +}) => { const [form] = Form.useForm(); useEffect(() => { - if (open) { - form.resetFields(); - if (job) { - form.setFieldsValue(job); - } + if (!open) return; + form.resetFields(); + if (job) { + form.setFieldsValue({ + ...job, + jobLocationAreaCode: String(job.jobLocationAreaCode || ''), + }); + } else { + form.setFieldsValue({ isPublish: 1, jobContactList: [{}] }); } }, [open, job, form]); @@ -29,35 +57,156 @@ const JobEditModal: React.FC = ({ open, fairId, company, job, onCancel, o title={job ? '编辑岗位' : `为「${company?.companyName || ''}」添加岗位`} form={form} open={open} - width={500} - modalProps={{ destroyOnClose: true, onCancel }} + width={900} + grid + rowProps={{ gutter: [16, 16] }} + colProps={{ span: 12 }} + modalProps={{ + destroyOnClose: true, + onCancel, + style: { height: '80vh' }, + styles: { + body: { + height: 'calc(80vh - 120px)', + overflowY: 'auto', + padding: '16px 24px', + }, + }, + }} onFinish={async (values) => { - if (job) { - const res = await updateJobForCompany({ ...job, ...values }); - if (res.code === 200) { onSuccess(); return true; } - } else if (company) { - const res = await addJobForCompany({ fairId, companyId: company.id, ...values }); - if (res.code === 200) { onSuccess(); return true; } + const payload = { + ...values, + companyId: company?.companyId, + companyName: company?.companyName, + jobContactList: values.jobContactList || [], + }; + const res = job + ? await updateJobForCompany(fairId, { ...job, ...payload }) + : await addJobForCompany({ fairId, companyId: company!.companyId, ...payload }); + if (res.code === 200) { + message.success(job ? '岗位修改成功' : '岗位添加成功'); + onSuccess(); + return true; } + message.error(res.msg || '操作失败'); return false; }} > - - - - - - - ({ label: v, value: v }))} - rules={[{ required: true }]} - /> - ({ label: v, value: v }))} - rules={[{ required: true }]} - /> - - + + + + + + + + + + + +
+
岗位联系人
+ + {(fields, { add, remove }) => ( + <> + {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 new file mode 100644 index 0000000..1b38e97 --- /dev/null +++ b/src/pages/Jobfair/Outdoorfair/Detail/components/ParticipatingCompaniesTab.tsx @@ -0,0 +1,313 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Button, Modal, Tag, 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 { + getParticipatingCompanies, + getParticipatingJobDetail, + getParticipatingJobs, + removeJobFromCompany, + removeParticipatingCompany, +} from '@/services/jobportal/outdoorFairDetail'; +import CompanyAddModal from './CompanyAddModal'; +import JobEditModal from './JobEditModal'; + +interface Props { + fairId: number; +} + +const ParticipatingCompaniesTab: React.FC = ({ fairId }) => { + const companyActionRef = useRef(); + const jobActionRef = useRef(); + const [companyModalOpen, setCompanyModalOpen] = useState(false); + const [jobListModalOpen, setJobListModalOpen] = useState(false); + const [jobEditModalOpen, setJobEditModalOpen] = useState(false); + const [selectedCompany, setSelectedCompany] = + useState(null); + 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({}); + + useEffect(() => { + Promise.all([ + getDictValueEnum('education', true, true), + getDictValueEnum('experience', true, true), + getDictValueEnum('area', true, true), + getDictValueEnum('job_type', true, true), + getDictValueEnum('scale', true, true), + ]).then(([education, experience, area, jobType, scale]) => { + setEducationEnum(education); + setExperienceEnum(experience); + setAreaEnum(area); + setJobTypeEnum(jobType); + setScaleEnum(scale); + }); + }, []); + + const openJobEditor = async (job?: API.OutdoorFairDetail.PostedJob) => { + if (job?.jobId) { + const res = await getParticipatingJobDetail(fairId, job.jobId); + if (res.code !== 200) { + message.error(res.msg || '岗位详情加载失败'); + return; + } + setEditingJob(res.data); + } else { + setEditingJob(null); + } + setJobEditModalOpen(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: 'isPublish', + hideInSearch: true, + width: 90, + render: (_, record) => ( + + {record.isPublish === 1 ? '已发布' : '未发布'} + + ), + }, + { + title: '操作', + valueType: 'option', + width: 130, + render: (_, record) => [ + , + , + ], + }, + ]; + + return ( + <> + + actionRef={companyActionRef} + rowKey="id" + headerTitle="参会企业" + search={{ labelWidth: 'auto' }} + options={false} + pagination={{ pageSize: 10 }} + toolBarRender={() => [ + , + ]} + request={async (params) => { + const res = await getParticipatingCompanies(fairId, { + current: params.current, + pageSize: params.pageSize, + companyName: params.companyName, + }); + return { data: res.rows || [], total: res.total || 0, success: res.code === 200 }; + }} + columns={[ + { title: '企业名称', dataIndex: 'companyName', ellipsis: true }, + { + title: '行业', + dataIndex: 'industry', + hideInSearch: true, + render: (_, record) => { + const v = record.industry; + if (!v) return '--'; + // 行业分类 id 均为 6 位编号,纯数字裸码(如 4、6)无对应分类项,兜底为“其他” + if (/^\d+$/.test(String(v))) return 其他; + return {v}; + }, + }, + { + title: '规模', + dataIndex: 'scale', + hideInSearch: true, + render: (_, record) => { + const v = record.scale; + if (v === undefined || v === null || v === '') return '其他'; + // 规模为字典码,渲染为标签;匹配不到时兜底“其他”,不显示裸数字 + const label = scaleEnum[v]?.label || scaleEnum[v]?.text; + return label ? {label} : '其他'; + }, + }, + { title: '联系人', dataIndex: 'contactPerson', hideInSearch: true }, + { title: '联系电话', dataIndex: 'contactPhone', hideInSearch: true }, + { + title: '展位号', + dataIndex: 'boothNumber', + hideInSearch: true, + render: (_, record) => record.boothNumber || '--', + }, + { title: '岗位数', dataIndex: 'jobCount', hideInSearch: true, width: 80 }, + { + title: '操作', + valueType: 'option', + width: 220, + render: (_, record) => [ + , + , + , + ], + }, + ]} + /> + + setJobListModalOpen(false)} + > + {selectedCompany && ( + + actionRef={jobActionRef} + rowKey="jobId" + search={{ labelWidth: 'auto' }} + options={false} + pagination={{ pageSize: 10 }} + toolBarRender={() => [ + , + ]} + request={async (params) => { + const res = await getParticipatingJobs(fairId, { + companyId: selectedCompany.companyId, + 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} + /> + )} + + + setCompanyModalOpen(false)} + onSuccess={() => { + setCompanyModalOpen(false); + companyActionRef.current?.reload(); + }} + /> + + { + setJobEditModalOpen(false); + setEditingJob(null); + }} + onSuccess={() => { + setJobEditModalOpen(false); + setEditingJob(null); + jobActionRef.current?.reload(); + companyActionRef.current?.reload(); + }} + /> + + ); +}; + +export default ParticipatingCompaniesTab; diff --git a/src/pages/Jobfair/Outdoorfair/Detail/components/ParticipatingJobsTab.tsx b/src/pages/Jobfair/Outdoorfair/Detail/components/ParticipatingJobsTab.tsx new file mode 100644 index 0000000..07d22be --- /dev/null +++ b/src/pages/Jobfair/Outdoorfair/Detail/components/ParticipatingJobsTab.tsx @@ -0,0 +1,200 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Button, Modal, Tag, message } from 'antd'; +import type { ActionType, ProColumns } from '@ant-design/pro-components'; +import { ProTable } from '@ant-design/pro-components'; +import { getDictValueEnum } from '@/services/system/dict'; +import { + getParticipatingCompanies, + getParticipatingJobDetail, + getParticipatingJobs, + removeJobFromCompany, +} from '@/services/jobportal/outdoorFairDetail'; +import JobEditModal from './JobEditModal'; + +interface Props { + fairId: number; +} + +const ParticipatingJobsTab: React.FC = ({ fairId }) => { + const actionRef = useRef(); + const [editingJob, setEditingJob] = useState(null); + const [editingCompany, setEditingCompany] = + useState(null); + const [modalOpen, setModalOpen] = useState(false); + const [educationEnum, setEducationEnum] = useState({}); + const [experienceEnum, setExperienceEnum] = useState({}); + const [areaEnum, setAreaEnum] = useState({}); + const [jobTypeEnum, setJobTypeEnum] = useState({}); + + 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 editJob = async (record: API.OutdoorFairDetail.PostedJob) => { + const res = await getParticipatingJobDetail(fairId, record.jobId!); + if (res.code !== 200) { + message.error(res.msg || '岗位详情加载失败'); + return; + } + setEditingJob(res.data); + setEditingCompany({ + id: 0, + fairId, + companyId: record.companyId, + companyName: record.companyName || '', + industry: '', + scale: '', + contactPerson: '', + contactPhone: '', + jobCount: 0, + }); + setModalOpen(true); + }; + + const columns: ProColumns[] = [ + { title: '岗位名称', dataIndex: 'jobTitle', ellipsis: true }, + { + title: '企业名称', + dataIndex: 'companyId', + valueType: 'select', + request: async ({ keyWords }) => { + const res = await getParticipatingCompanies(fairId, { + current: 1, + pageSize: 100, + companyName: keyWords, + }); + return Array.from( + new Map( + (res.rows || []).map((company) => [ + company.companyId, + { label: company.companyName, value: company.companyId }, + ]), + ).values(), + ); + }, + fieldProps: { + showSearch: true, + optionFilterProp: 'label', + placeholder: '请选择或搜索企业', + }, + render: (_, record) => record.companyName || '--', + 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: 'jobLocation', hideInSearch: true, ellipsis: true }, + { + title: '发布状态', + dataIndex: 'isPublish', + hideInSearch: true, + width: 90, + render: (_, record) => ( + + {record.isPublish === 1 ? '已发布' : '未发布'} + + ), + }, + { + title: '操作', + valueType: 'option', + width: 130, + render: (_, record) => [ + , + , + ], + }, + ]; + + return ( + <> + + actionRef={actionRef} + rowKey="jobId" + headerTitle="参会岗位" + search={{ labelWidth: 'auto', defaultCollapsed: false }} + options={false} + pagination={{ pageSize: 10 }} + request={async (params) => { + const res = await getParticipatingJobs(fairId, { + current: params.current, + pageSize: params.pageSize, + companyId: params.companyId, + jobTitle: params.jobTitle, + education: params.education, + experience: params.experience, + }); + return { data: res.rows || [], total: res.total || 0, success: res.code === 200 }; + }} + columns={columns} + /> + + setModalOpen(false)} + onSuccess={() => { + setModalOpen(false); + actionRef.current?.reload(); + }} + /> + + ); +}; + +export default ParticipatingJobsTab; diff --git a/src/pages/Jobfair/Outdoorfair/Detail/components/StatisticsTab.tsx b/src/pages/Jobfair/Outdoorfair/Detail/components/StatisticsTab.tsx deleted file mode 100644 index 4e6262e..0000000 --- a/src/pages/Jobfair/Outdoorfair/Detail/components/StatisticsTab.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react'; -import { Card, Col, Row, Statistic, Empty, Spin, Table } from 'antd'; -import { TeamOutlined, FileTextOutlined, UserOutlined, LinkOutlined } from '@ant-design/icons'; - -interface Props { - fairId: number; -} - -const StatisticsTab: React.FC = () => { - // 静态演示数据 - const companyCount = 25; - const jobCount = 86; - const demandCount = 520; - const intentionCount = 180; - - const dailyData = [ - { key: '1', date: '2026-03-15', count: 320 }, - ]; - - const industryData = [ - { key: '1', industry: '化工', count: 5 }, - { key: '2', industry: '政府', count: 2 }, - { key: '3', industry: '农业', count: 4 }, - { key: '4', industry: '医疗', count: 3 }, - { key: '5', industry: '教育', count: 3 }, - { key: '6', industry: '制造', count: 4 }, - ]; - - const jobTypeData = [ - { key: '1', type: '技术类', count: 30 }, - { key: '2', type: '管理类', count: 15 }, - { key: '3', type: '销售类', count: 12 }, - { key: '4', type: '行政类', count: 10 }, - { key: '5', type: '一线工人', count: 14 }, - { key: '6', type: '其他', count: 5 }, - ]; - - const timeSlotData = [ - { key: '1', slot: '08:00-09:00', count: 80 }, - { key: '2', slot: '09:00-10:00', count: 120 }, - { key: '3', slot: '10:00-11:00', count: 65 }, - { key: '4', slot: '11:00-12:00', count: 30 }, - ]; - - return ( -
- - - - } suffix="家" /> - - - - - } suffix="个" /> - - - - - } suffix="人" /> - - - - - } suffix="人" /> - - - - - - - - `${v} 人` }, - ]} dataSource={dailyData} pagination={false} size="small" /> - - - - -
`${v} 家` }, - ]} dataSource={industryData} pagination={false} size="small" /> - - - - -
`${v} 个` }, - ]} dataSource={jobTypeData} pagination={false} size="small" /> - - - - -
`${v} 人` }, - ]} dataSource={timeSlotData} pagination={false} size="small" /> - - - - - ); -}; - -export default StatisticsTab; diff --git a/src/pages/Jobfair/Outdoorfair/Detail/index.tsx b/src/pages/Jobfair/Outdoorfair/Detail/index.tsx index d237b0e..fd47220 100644 --- a/src/pages/Jobfair/Outdoorfair/Detail/index.tsx +++ b/src/pages/Jobfair/Outdoorfair/Detail/index.tsx @@ -7,10 +7,8 @@ import { getOutdoorFairInfo } from '@/services/jobportal/outdoorFair'; import type { OutdoorFairItem } from '@/services/jobportal/outdoorFair'; import FairInfoTab from './components/FairInfoTab'; import BoothTab from './components/BoothTab'; -import CompanyReviewTab from './components/CompanyReviewTab'; -import AttendeeTab from './components/AttendeeTab'; -import DeviceTab from './components/DeviceTab'; -import StatisticsTab from './components/StatisticsTab'; +import ParticipatingCompaniesTab from './components/ParticipatingCompaniesTab'; +import ParticipatingJobsTab from './components/ParticipatingJobsTab'; const OutdoorFairDetail: React.FC = () => { const [searchParams] = useSearchParams(); @@ -18,7 +16,6 @@ const OutdoorFairDetail: React.FC = () => { const [fairInfo, setFairInfo] = useState(null); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState('fair-info'); - const [fairInfoRefreshKey, setFairInfoRefreshKey] = useState(0); const fetchFairInfo = async () => { setLoading(true); @@ -42,18 +39,13 @@ const OutdoorFairDetail: React.FC = () => { const tabItems = [ { key: 'fair-info', label: '招聘会管理' }, + { key: 'participating-companies', label: '参会企业' }, + { key: 'participating-jobs', label: '参会岗位' }, { key: 'booth', label: '展位图' }, - { key: 'company-review', label: '参会单位管理' }, - { key: 'attendee', label: '参会人员管理' }, - { key: 'device', label: '入场设备管理' }, - { key: 'statistics', label: '数据统计' }, ]; const handleTabChange = (key: string) => { setActiveTab(key); - if (key === 'fair-info') { - setFairInfoRefreshKey((prev) => prev + 1); - } }; return ( @@ -83,20 +75,15 @@ const OutdoorFairDetail: React.FC = () => { ); case 'booth': return ; - case 'company-review': - return ; - case 'attendee': - return ; - case 'device': - return ; - case 'statistics': - return ; + case 'participating-companies': + return ; + case 'participating-jobs': + return ; default: return null; } diff --git a/src/pages/Jobfair/Outdoorfair/index.tsx b/src/pages/Jobfair/Outdoorfair/index.tsx index e283c25..b0ed86c 100644 --- a/src/pages/Jobfair/Outdoorfair/index.tsx +++ b/src/pages/Jobfair/Outdoorfair/index.tsx @@ -125,7 +125,16 @@ const OutdoorFairList: React.FC = () => { title: '绑定场地', dataIndex: 'venueName', ellipsis: true, - hideInSearch: true, + valueType: 'select', + fieldProps: { + options: venueOptions, + allowClear: true, + showSearch: true, + optionFilterProp: 'label', + }, + search: { + transform: (value) => ({ venueId: value }), + }, render: (_, record) => record.venueId && record.venueName ? ( , + , + ], + }} + > + + {venueInfo ? ( + + + {venueInfo.venueName || '--'} + + + {venueTypeLabel(venueInfo.venueType)} + + + {venueInfo.venueArea != null ? `${venueInfo.venueArea} ㎡` : '--'} + + + {venueInfo.floorCount ?? '--'} + + + {venueInfo.venueAddress || '--'} + + + {venueInfo.contactPhone || '--'} + + + {venueInfo.routeLineNames || '--'} + + + {venueInfo.handleDate || '--'} + + + {venueInfo.handleOrg || '--'} + + + {venueInfo.handler || '--'} + + + {venueInfo.remark || '--'} + + + ) : ( + !loading &&
未找到该场地信息
+ )} +
+ + setEditModalOpen(false)} + onSubmit={async (values: VenueInfoForm) => { + const res = await updateVenueInfo(values); + if (res.code === 200) { + message.success('修改成功'); + setEditModalOpen(false); + fetchVenueInfo(); + } else { + message.error(res.msg || '操作失败'); + } + }} + /> + + ); +}; + +export default VenueInfoDetail; diff --git a/src/services/jobportal/outdoorFair.ts b/src/services/jobportal/outdoorFair.ts index 76cdf21..357af15 100644 --- a/src/services/jobportal/outdoorFair.ts +++ b/src/services/jobportal/outdoorFair.ts @@ -42,6 +42,7 @@ export interface OutdoorFairListParams { fairType?: string; region?: string; venueId?: number; + onlineApply?: boolean; current?: number; pageSize?: number; } diff --git a/src/services/jobportal/outdoorFairDetail.ts b/src/services/jobportal/outdoorFairDetail.ts index 77812a7..12e70fe 100644 --- a/src/services/jobportal/outdoorFairDetail.ts +++ b/src/services/jobportal/outdoorFairDetail.ts @@ -1011,7 +1011,11 @@ function paginate(arr: T[], current: number, pageSize: number) { /** 获取参会企业列表 */ export async function getParticipatingCompanies( fairId: number, - params?: { current?: number; pageSize?: number; companyName?: string }, + params?: { + current?: number; + pageSize?: number; + companyName?: string; + }, ) { return request<{ code: number; msg?: string; total: number; rows: ParticipatingCompany[] }>( `/api/cms/outdoor-fair/${fairId}/companies`, @@ -1022,6 +1026,36 @@ export async function getParticipatingCompanies( ); } +/** 分页获取招聘会参会岗位,可按企业懒加载。 */ +export async function getParticipatingJobs( + fairId: number, + params?: { + current?: number; + pageSize?: number; + companyId?: number; + companyName?: string; + jobTitle?: string; + education?: string; + experience?: string; + }, +) { + return request<{ code: number; msg?: string; total: number; rows: PostedJob[] }>( + `/api/cms/outdoor-fair/${fairId}/jobs`, + { + method: 'GET', + params, + }, + ); +} + +/** 获取招聘会岗位详情(编辑时才加载联系人等完整字段)。 */ +export async function getParticipatingJobDetail(fairId: number, jobId: number) { + return request<{ code: number; msg?: string; data: PostedJob }>( + `/api/cms/outdoor-fair/${fairId}/jobs/${jobId}`, + { method: 'GET' }, + ); +} + /** 添加参会企业 */ export async function addParticipatingCompany(data: { fairId: number; @@ -1058,46 +1092,40 @@ export async function removeParticipatingCompany(id: number, fairId?: number) { export async function addJobForCompany(data: { fairId: number; companyId: number; - jobTitle: string; - minSalary: number; - maxSalary: number; - education: string; - experience: string; - vacancies: number; + [key: string]: any; }) { - const company = MOCK_COMPANIES.find((c) => c.id === data.companyId); - if (!company) return { code: 404, msg: '企业未找到' }; - const job: PostedJob = { - ...data, - id: nextPostedJobId++, - status: 'active', - }; - company.jobList.push(job); - return { code: 200, msg: '岗位添加成功', data: job }; + const { fairId, companyId, ...job } = data; + return request<{ code: number; msg?: string; data?: PostedJob }>( + `/api/cms/outdoor-fair/${fairId}/companies/${companyId}/jobs`, + { + method: 'POST', + data: job, + }, + ); } /** 编辑岗位 */ -export async function updateJobForCompany(data: PostedJob) { - for (const c of MOCK_COMPANIES) { - const idx = c.jobList.findIndex((j) => j.id === data.id); - if (idx !== -1) { - c.jobList[idx] = { ...c.jobList[idx], ...data }; - return { code: 200, msg: '修改成功' }; - } +export async function updateJobForCompany(fairId: number, data: PostedJob) { + if (!data.jobId) { + return { code: 400, msg: '缺少岗位ID' }; } - return { code: 404, msg: '未找到' }; + return request<{ code: number; msg?: string }>( + `/api/cms/outdoor-fair/${fairId}/jobs/${data.jobId}`, + { + method: 'PUT', + data, + }, + ); } /** 删除岗位 */ -export async function removeJobFromCompany(jobId: number) { - for (const c of MOCK_COMPANIES) { - const idx = c.jobList.findIndex((j) => j.id === jobId); - if (idx !== -1) { - c.jobList.splice(idx, 1); - return { code: 200, msg: '删除成功' }; - } - } - return { code: 404, msg: '未找到' }; +export async function removeJobFromCompany(fairId: number, jobId: number) { + return request<{ code: number; msg?: string }>( + `/api/cms/outdoor-fair/${fairId}/jobs/${jobId}`, + { + method: 'DELETE', + }, + ); } // ==================== 模块2: 展位管理 API ==================== diff --git a/src/types/jobfair/outdoorFairDetail.d.ts b/src/types/jobfair/outdoorFairDetail.d.ts index f555d17..0eeee4d 100644 --- a/src/types/jobfair/outdoorFairDetail.d.ts +++ b/src/types/jobfair/outdoorFairDetail.d.ts @@ -17,21 +17,32 @@ declare namespace API.OutdoorFairDetail { contactPhone: string; boothId?: number; boothNumber?: string; - jobList: PostedJob[]; + jobCount?: number; + /** 仅兼容旧 mock 数据,真实企业接口不再返回岗位列表。 */ + jobList?: PostedJob[]; } /** 已发布岗位 */ export interface PostedJob { - id: number; - fairId: number; + /** 真实岗位主键;旧 mock 数据仅使用 id。 */ + jobId?: number; + id?: number; + fairId?: number; companyId: number; + companyName?: string; jobTitle: string; minSalary: number; maxSalary: number; education: string; experience: string; vacancies: number; - status: string; // 'active' | 'closed' + jobLocationAreaCode?: string; + jobType?: string; + jobLocation?: string; + description?: string; + isPublish?: number; + status?: string; + jobContactList?: API.ManagementList.ContactPerson[]; } // ==================== 模块2: 展位管理 ====================