From 9012b0c4a7b08ca1b4f5780a888d755a9158977c Mon Sep 17 00:00:00 2001 From: lapuda <577732344@qq.com> Date: Thu, 25 Jun 2026 16:07:43 +0800 Subject: [PATCH] Refactor venue booth functions for outdoor fairs - Renamed `getVenueBoothList` to `getOutdoorFairBoothMap` for clarity. - Updated the endpoint to fetch booth maps for outdoor fairs. - Renamed `saveVenueBoothList` to `saveOutdoorFairBoothMap` to reflect its purpose. - Adjusted the endpoint for saving booth maps for outdoor fairs. - Removed the redundant `getOutdoorFairBoothMap` function as it was previously defined. --- config/routes.ts | 49 +- .../Detail/components/BoothTab.tsx | 541 ++++++- .../Detail/components/CompanyAddModal.tsx | 260 +++- .../Detail/components/FairInfoTab.tsx | 9 +- .../Jobfair/Outdoorfair/Detail/index.tsx | 11 +- src/pages/Jobfair/Venueinfo/Detail/index.tsx | 402 ------ src/pages/Jobfair/Venueinfo/index.tsx | 16 +- src/services/jobportal/outdoorFairDetail.ts | 1263 ++++++++++++++--- src/services/jobportal/venueInfo.ts | 21 +- 9 files changed, 1804 insertions(+), 768 deletions(-) delete mode 100644 src/pages/Jobfair/Venueinfo/Detail/index.tsx diff --git a/config/routes.ts b/config/routes.ts index 68035a2..325571a 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -37,7 +37,7 @@ export default [ ], }, { - path: '/job-portal',// 求职者首页 + path: '/job-portal', // 求职者首页 layout: false, routes: [ { @@ -45,63 +45,63 @@ export default [ component: './JobPortal', }, { - path: '/job-portal/list',// 职位列表 + path: '/job-portal/list', // 职位列表 component: './JobPortal/List', }, { - path: '/job-portal/detail',// 职位详情 + path: '/job-portal/detail', // 职位详情 component: './JobPortal/Detail', }, { - path: '/job-portal/resume',// 简历页面(PC布局) + path: '/job-portal/resume', // 简历页面(PC布局) component: './JobPortal/Resume', }, { - path: '/job-portal/profile',// 求职者我的页面 + path: '/job-portal/profile', // 求职者我的页面 component: './JobPortal/Profile', }, { - path: '/job-portal/personal-center',// 个人中心页面 + path: '/job-portal/personal-center', // 个人中心页面 component: './JobPortal/PersonalCenter', }, { - path: '/job-portal/personal-center/applications',// 已投递岗位列表 + path: '/job-portal/personal-center/applications', // 已投递岗位列表 component: './JobPortal/PersonalCenter/Applications', }, { - path: '/job-portal/personal-center/favorites',// 收藏岗位列表 + path: '/job-portal/personal-center/favorites', // 收藏岗位列表 component: './JobPortal/PersonalCenter/Favorites', }, { - path: '/job-portal/personal-center/footprints',// 访问足迹岗位列表 + path: '/job-portal/personal-center/footprints', // 访问足迹岗位列表 component: './JobPortal/PersonalCenter/Footprints', }, { - path: '/job-portal/personal-center/showcase',// 我的风采 + path: '/job-portal/personal-center/showcase', // 我的风采 component: './JobPortal/PersonalCenter/Showcase', }, { - path: '/job-portal/personal-center/interviews',// 我的面试邀约 + path: '/job-portal/personal-center/interviews', // 我的面试邀约 component: './JobPortal/PersonalCenter/Interviews', }, { - path: '/job-portal/message',// 消息通知页面 + path: '/job-portal/message', // 消息通知页面 component: './JobPortal/Message', }, { - path: '/job-portal/career-recommendation',// 职业推荐 + path: '/job-portal/career-recommendation', // 职业推荐 component: './JobPortal/CareerRecommendation', }, { - path: '/job-portal/policy',// 政策列表 + path: '/job-portal/policy', // 政策列表 component: './JobPortal/Policy', }, { - path: '/job-portal/policy/detail',// 政策详情 + path: '/job-portal/policy/detail', // 政策详情 component: './JobPortal/Policy/Detail', }, { - path: '/job-portal/live-recruitment',// 直播带岗 + path: '/job-portal/live-recruitment', // 直播带岗 component: './JobPortal/LiveRecruitment', }, ], @@ -231,11 +231,6 @@ 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', @@ -246,16 +241,6 @@ export default [ path: '/jobfair/cross-city-fair', component: './Jobfair/Crosscityfair', }, - { - name: '室内固定摊位招聘会管理', - path: '/jobfair/indoor-job-fair', - component: './Jobfair/Indoorjobfair', - }, - { - name: '室内固定摊位招聘会详情', - path: '/jobfair/indoor-job-fair/detail', - component: './Jobfair/Indoorjobfair/Detail', - }, ], }, { @@ -270,7 +255,7 @@ export default [ ], }, { - path: '/management/enterprise-showcase',// 企业风采 + path: '/management/enterprise-showcase', // 企业风采 layout: false, component: './Management/EnterpriseShowcase', }, diff --git a/src/pages/Jobfair/Outdoorfair/Detail/components/BoothTab.tsx b/src/pages/Jobfair/Outdoorfair/Detail/components/BoothTab.tsx index ec27231..bdc6e94 100644 --- a/src/pages/Jobfair/Outdoorfair/Detail/components/BoothTab.tsx +++ b/src/pages/Jobfair/Outdoorfair/Detail/components/BoothTab.tsx @@ -1,12 +1,31 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useAccess } from '@umijs/max'; -import { Button, Card, Col, Empty, Modal, Row, Select, Space, Tag, Tooltip, message } from 'antd'; +import { + Button, + Card, + Col, + Empty, + Input, + InputNumber, + List, + Modal, + Popover, + Row, + Select, + Space, + Tabs, + Tag, + Tooltip, + message, +} from 'antd'; import { ReloadOutlined, SwapOutlined } from '@ant-design/icons'; +import { Rnd } from 'react-rnd'; import { getCmsCompanyList } from '@/services/company/list'; import { bookOutdoorFairBooth, cancelOutdoorFairBooth, getOutdoorFairBoothMap, + saveOutdoorFairBoothMap, swapOutdoorFairBooth, } from '@/services/jobportal/venueInfo'; import type { VenueBoothItem } from '@/services/jobportal/venueInfo'; @@ -17,6 +36,14 @@ interface Props { fairInfo: OutdoorFairItem; } +const BOOTH_WIDTH = 70; +const BOOTH_HEIGHT = 44; +const BOOTH_GAP_X = 42; +const BOOTH_GAP_Y = 54; +const BOOTH_START_X = 40; +const BOOTH_START_Y = 40; +const BOOTHS_PER_ROW = 10; + const BoothMapTab: React.FC = ({ fairId, fairInfo }) => { const access = useAccess(); const [booths, setBooths] = useState([]); @@ -26,6 +53,51 @@ const BoothMapTab: React.FC = ({ fairId, fairInfo }) => { const [bookingModalOpen, setBookingModalOpen] = useState(false); const [companyId, setCompanyId] = useState(); const [swapBoothIds, setSwapBoothIds] = useState([]); + const [boothModalOpen, setBoothModalOpen] = useState(false); + const [editingBoothKey, setEditingBoothKey] = useState(); + const [boothNumber, setBoothNumber] = useState(''); + const [generateModalOpen, setGenerateModalOpen] = useState(false); + const [generateRows, setGenerateRows] = useState(2); + const [generateColumns, setGenerateColumns] = useState(10); + const [activeBoothKey, setActiveBoothKey] = useState(); + const [hoveredBoothKey, setHoveredBoothKey] = useState(); + + const getBoothKey = (booth: VenueBoothItem, index: number) => + String(booth.boothId || booth.id || `${booth.boothNumber}-${index}`); + + const canvasSize = useMemo(() => { + const maxX = booths.reduce((max, booth) => Math.max(max, booth.positionX || 0), 0); + const maxY = booths.reduce((max, booth) => Math.max(max, booth.positionY || 0), 0); + return { + width: Math.max(960, maxX + BOOTH_WIDTH + BOOTH_START_X), + height: Math.max(520, maxY + BOOTH_HEIGHT + BOOTH_START_Y), + }; + }, [booths]); + + const arrangeBooths = (items: VenueBoothItem[], columns = BOOTHS_PER_ROW) => + items.map((booth, index) => ({ + ...booth, + positionX: BOOTH_START_X + (index % columns) * (BOOTH_WIDTH + BOOTH_GAP_X), + positionY: BOOTH_START_Y + Math.floor(index / columns) * (BOOTH_HEIGHT + BOOTH_GAP_Y), + })); + + const getNextBoothNumber = () => { + const existingNumbers = new Set(booths.map((item) => item.boothNumber)); + let next = booths.length + 1; + while (existingNumbers.has(`KJ${String(next).padStart(2, '0')}`)) { + next += 1; + } + return `KJ${String(next).padStart(2, '0')}`; + }; + + const getNextBoothPosition = () => { + const columns = Math.max(1, generateColumns || BOOTHS_PER_ROW); + const index = booths.length; + return { + positionX: BOOTH_START_X + (index % columns) * (BOOTH_WIDTH + BOOTH_GAP_X), + positionY: BOOTH_START_Y + Math.floor(index / columns) * (BOOTH_HEIGHT + BOOTH_GAP_Y), + }; + }; const fetchData = async () => { setLoading(true); @@ -35,7 +107,13 @@ const BoothMapTab: React.FC = ({ fairId, fairInfo }) => { getCmsCompanyList({ current: 1, pageSize: 999, status: 1 } as API.CompanyList.Params), ]); if (boothRes.code === 200) { - setBooths(boothRes.data || []); + const fetchedBooths = boothRes.data || []; + setBooths(fetchedBooths); + if (fetchedBooths.length > 0) { + const columns = Math.min(BOOTHS_PER_ROW, fetchedBooths.length); + setGenerateColumns(columns); + setGenerateRows(Math.max(1, Math.ceil(fetchedBooths.length / columns))); + } } if (companyRes.code === 200) { setCompanies(companyRes.rows || []); @@ -67,12 +145,124 @@ const BoothMapTab: React.FC = ({ fairId, fairInfo }) => { [companies], ); + const boothList = useMemo( + () => + [...booths].sort((a, b) => + String(a.boothNumber || '').localeCompare(String(b.boothNumber || ''), 'zh-CN', { + numeric: true, + }), + ), + [booths], + ); + const openBookingModal = (booth: VenueBoothItem) => { setSelectedBooth(booth); setCompanyId(booth.companyId); setBookingModalOpen(true); }; + const openBoothModal = (booth?: VenueBoothItem, index?: number) => { + setEditingBoothKey(booth && index !== undefined ? getBoothKey(booth, index) : undefined); + setBoothNumber(booth?.boothNumber || getNextBoothNumber()); + setBoothModalOpen(true); + }; + + const handleSaveBoothNumber = () => { + const trimmed = boothNumber.trim(); + if (!trimmed) { + message.warning('请输入展位号'); + return; + } + if ( + booths.some( + (booth, index) => + booth.boothNumber === trimmed && getBoothKey(booth, index) !== editingBoothKey, + ) + ) { + message.warning('展位号不能重复'); + return; + } + if (editingBoothKey) { + setBooths( + booths.map((booth, index) => + getBoothKey(booth, index) === editingBoothKey + ? { ...booth, boothNumber: trimmed } + : booth, + ), + ); + } else { + const nextPosition = getNextBoothPosition(); + setBooths([ + ...booths, + { + boothNumber: trimmed, + positionX: nextPosition.positionX, + positionY: nextPosition.positionY, + status: 'available', + }, + ]); + } + setBoothModalOpen(false); + }; + + const handleMoveBooth = (key: string, x: number, y: number) => { + setBooths((prev) => + prev.map((booth, index) => + getBoothKey(booth, index) === key + ? { ...booth, positionX: Math.round(x), positionY: Math.round(y) } + : booth, + ), + ); + }; + + const handleDeleteBooth = (key: string) => { + const target = booths.find((booth, index) => getBoothKey(booth, index) === key); + if (target?.status === 'reserved') { + message.warning('已预定展位不能删除,请先取消预定'); + return; + } + setBooths(booths.filter((booth, index) => getBoothKey(booth, index) !== key)); + setActiveBoothKey(undefined); + }; + + const handleGenerateBooths = () => { + const rows = Math.max(1, generateRows || 1); + const columns = Math.max(1, generateColumns || 1); + const count = rows * columns; + const existingByNumber = new Map(booths.map((item) => [item.boothNumber, item])); + const generated = Array.from({ length: count }).map((_, index) => { + const boothNumber = `KJ${String(index + 1).padStart(2, '0')}`; + const existing = existingByNumber.get(boothNumber); + return { + ...existing, + boothNumber, + positionX: BOOTH_START_X + (index % columns) * (BOOTH_WIDTH + BOOTH_GAP_X), + positionY: BOOTH_START_Y + Math.floor(index / columns) * (BOOTH_HEIGHT + BOOTH_GAP_Y), + status: existing?.status || 'available', + }; + }); + setBooths(generated); + setGenerateModalOpen(false); + message.success( + `已生成 ${rows} 行 × ${columns} 列,共 ${count} 个展位,确认后请点击保存展位图`, + ); + }; + + const handleArrangeBooths = () => { + setBooths(arrangeBooths(booths, Math.max(1, generateColumns || BOOTHS_PER_ROW))); + message.success('已整理布局,确认后请点击保存展位图'); + }; + + const handleSaveBooths = async () => { + const res = await saveOutdoorFairBoothMap(fairId, booths); + if (res.code === 200) { + message.success('展位图保存成功'); + fetchData(); + } else { + message.error(res.msg || '展位图保存失败'); + } + }; + const handleBookBooth = async () => { if (!selectedBooth?.boothId && !selectedBooth?.id) { return; @@ -149,13 +339,17 @@ const BoothMapTab: React.FC = ({ fairId, fairInfo }) => { }; return ( - - - + + + +
+ + + + + - } - > +
{!fairInfo.venueId ? ( ) : booths.length === 0 ? ( - + ) : (
= ({ fairId, fairInfo }) => { border: '1px solid #f0f0f0', }} > - {booths.map((booth) => { - const boothId = Number(booth.boothId || booth.id); - const reserved = booth.status === 'reserved'; - const selectedForSwap = swapBoothIds.includes(boothId); - return ( - -
toggleSwapBooth(booth)} - style={{ - position: 'absolute', - left: booth.positionX, - top: booth.positionY, - width: 52, - minHeight: 30, - padding: '4px 2px', - textAlign: 'center', - borderRadius: 4, - cursor: 'pointer', - color: '#fff', - background: reserved ? '#52c41a' : '#1890ff', - outline: selectedForSwap ? '3px solid #fa8c16' : undefined, - boxShadow: selectedForSwap ? '0 0 0 2px #fff inset' : undefined, - fontSize: 12, - }} +
+ {booths.map((booth, index) => { + const boothId = Number(booth.boothId || booth.id); + const reserved = booth.status === 'reserved'; + const selectedForSwap = swapBoothIds.includes(boothId); + const key = getBoothKey(booth, index); + return ( + setActiveBoothKey(open ? key : undefined)} + title={`${booth.boothNumber}:${reserved ? '已预定' : '未预定'}`} + content={ + + {reserved && ( + +
+ {booth.companyName || '暂无企业名称'} +
+
+ )} + + + + {reserved && ( + + )} +
+ } > -
{booth.boothNumber}
-
- -
+
{booth.boothNumber}
+
{reserved ? '改单位' : '预定'}
+
+ + + ); + })} + {booths.map((booth, index) => { + const key = getBoothKey(booth, index); + const showCompanyName = + hoveredBoothKey === key && booth.status === 'reserved' && booth.companyName; + if (!showCompanyName) { + return null; + } + return ( +
+ {booth.companyName}
- - ); - })} + ); + })} +
)}
- - - - - 已预定({stats.reserved}) - - - 未安排({stats.available}) - - - 总展位({stats.total}) - - -
- 提示:点击两个绿色已预定展位后,可点击“调换位置”交换预定单位。 -
+ + + + + + 已预定({stats.reserved}) + + + 未安排({stats.available}) + + + 总展位({stats.total}) + + +
+ 提示:拖动展位可调整副本布局;点击两个绿色已预定展位后,可点击“调换位置”交换预定单位。 +
+ + ), + }, + { + key: 'list', + label: '展位列表', + children: + boothList.length === 0 ? ( + + ) : ( + { + const reserved = booth.status === 'reserved'; + return ( + + + + {booth.boothNumber} + + {reserved ? '已预定' : '未预定'} + + + +
+ {reserved + ? booth.companyName || '已预定,暂无企业名称' + : '暂无预定企业'} +
+
+
+
+ ); + }} + /> + ), + }, + ]} + />
+ setGenerateModalOpen(false)} + onOk={handleGenerateBooths} + okText="生成" + > + + + 行数 + setGenerateRows(Number(value) || 1)} + /> + 列数 + setGenerateColumns(Number(value) || 1)} + /> + +
+ 将生成 {Math.max(1, generateRows || 1)} 行 × {Math.max(1, generateColumns || 1)} 列,共{' '} + {Math.max(1, generateRows || 1) * Math.max(1, generateColumns || 1)}{' '} + 个展位。生成后需要点击“保存展位图”才会写入招聘会副本。 +
+
+
+ setBoothModalOpen(false)} + onOk={handleSaveBoothNumber} + okText="确定" + > + setBoothNumber(event.target.value)} + onPressEnter={handleSaveBoothNumber} + /> + = ({ open, fairId, onCancel, onSuccess }) const [selectedRows, setSelectedRows] = useState([]); const [scaleEnum, setScaleEnum] = useState>({}); const [industryEnum, setIndustryEnum] = useState({}); + const [boothModalOpen, setBoothModalOpen] = useState(false); + const [boothLoading, setBoothLoading] = useState(false); + const [booths, setBooths] = useState([]); + const [selectedBoothId, setSelectedBoothId] = useState(); + + const canvasSize = useMemo(() => { + const maxX = booths.reduce((max, booth) => Math.max(max, booth.positionX || 0), 0); + const maxY = booths.reduce((max, booth) => Math.max(max, booth.positionY || 0), 0); + return { + width: Math.max(760, maxX + 120), + height: Math.max(420, maxY + 90), + }; + }, [booths]); useEffect(() => { if (open) { @@ -86,70 +101,199 @@ const CompanyAddModal: React.FC = ({ open, fairId, onCancel, onSuccess }) const handleOk = async () => { if (selectedRows.length === 0) { - message.warning('请选择至少一家企业'); + message.warning('请选择一家企业'); return; } - let success = 0; - for (const row of selectedRows) { - const res = await addParticipatingCompany({ - fairId, - companyId: row.companyId, - companyName: row.name, - industry: getEnumLabel(industryEnum, row.industry), - scale: getEnumLabel(scaleEnum, row.scale), - contactPerson: row.contactPerson || row.companyContactList?.[0]?.contactPerson || '', - contactPhone: - row.contactPersonPhone || row.companyContactList?.[0]?.contactPersonPhone || '', - }); - if (res.code === 200) success++; + setBoothLoading(true); + setSelectedBoothId(undefined); + setBoothModalOpen(true); + try { + const res = await getOutdoorFairBoothMap(fairId); + if (res.code === 200) { + setBooths(res.data || []); + } else { + message.error(res.msg || '获取展位图失败'); + } + } finally { + setBoothLoading(false); } - message.success(`成功添加 ${success} 家企业`); + }; + + const handleConfirmBooth = async () => { + const row = selectedRows[0]; + if (!row) { + message.warning('请选择一家企业'); + return; + } + const booth = booths.find((item) => Number(item.boothId || item.id) === selectedBoothId); + if (!booth) { + message.warning('请选择展位'); + return; + } + if (booth.status === 'reserved') { + message.warning('该展位已被预定,请选择空展位'); + return; + } + + const addRes = await addParticipatingCompany({ + fairId, + companyId: row.companyId, + companyName: row.name, + industry: getEnumLabel(industryEnum, row.industry), + scale: getEnumLabel(scaleEnum, row.scale), + contactPerson: row.contactPerson || row.companyContactList?.[0]?.contactPerson || '', + contactPhone: row.contactPersonPhone || row.companyContactList?.[0]?.contactPersonPhone || '', + }); + if (addRes.code !== 200) { + message.error(addRes.msg || '添加企业失败'); + return; + } + + const bookRes = await bookOutdoorFairBooth(fairId, { + boothId: Number(booth.boothId || booth.id), + companyId: row.companyId, + companyName: row.name, + status: 'reserved', + }); + if (bookRes.code !== 200) { + message.error(bookRes.msg || '预定展位失败'); + return; + } + + message.success(`已添加企业并预定展位 ${booth.boothNumber}`); + setBoothModalOpen(false); setSelectedRows([]); + setSelectedBoothId(undefined); onSuccess(); }; + const resetAndCancel = () => { + setSelectedRows([]); + setSelectedBoothId(undefined); + setBoothModalOpen(false); + onCancel(); + }; + return ( - { - setSelectedRows([]); - onCancel(); - }} - onOk={handleOk} - okText="确认添加" - > - { - const res = await getCmsCompanyList({ - current: params.current, - pageSize: params.pageSize, - name: params.name, - industry: params.industry, - scale: params.scale, - status: 1, - } as API.CompanyList.Params); - return { - data: res.rows as any, - total: res.total, - success: res.code === 200, - }; - }} - rowSelection={{ - selectedRowKeys: selectedRows.map((r) => r.companyId), - onChange: (_, rows) => setSelectedRows(rows), - }} - tableAlertRender={false} - /> - + <> + + { + const res = await getCmsCompanyList({ + current: params.current, + pageSize: params.pageSize, + name: params.name, + industry: params.industry, + scale: params.scale, + status: 1, + } as API.CompanyList.Params); + return { + data: res.rows as any, + total: res.total, + success: res.code === 200, + }; + }} + rowSelection={{ + type: 'radio', + selectedRowKeys: selectedRows.map((r) => r.companyId), + onChange: (_, rows) => setSelectedRows(rows), + }} + tableAlertRender={false} + /> + + setBoothModalOpen(false)} + onOk={handleConfirmBooth} + okText="确认添加并预定" + > + + {booths.length === 0 ? ( + + ) : ( +
+
+ {booths.map((booth) => { + const boothId = Number(booth.boothId || booth.id); + const reserved = booth.status === 'reserved'; + const selected = selectedBoothId === boothId; + return ( + +
{ + if (reserved) { + message.warning('该展位已被预定,请选择空展位'); + return; + } + setSelectedBoothId(boothId); + }} + style={{ + position: 'absolute', + left: booth.positionX, + top: booth.positionY, + width: 70, + height: 44, + lineHeight: '44px', + textAlign: 'center', + borderRadius: 6, + color: '#fff', + cursor: reserved ? 'not-allowed' : 'pointer', + background: reserved ? '#52c41a' : '#1890ff', + opacity: reserved ? 0.7 : 1, + outline: selected ? '3px solid #fa8c16' : undefined, + boxShadow: selected + ? '0 0 0 2px #fff inset' + : '0 2px 6px rgba(0,0,0,0.12)', + userSelect: 'none', + }} + > + {booth.boothNumber} +
+
+ ); + })} +
+
+ )} +
+ 蓝色为空展位,可选择;绿色为已预定展位,不可选择。 +
+
+
+ ); }; diff --git a/src/pages/Jobfair/Outdoorfair/Detail/components/FairInfoTab.tsx b/src/pages/Jobfair/Outdoorfair/Detail/components/FairInfoTab.tsx index ead4c35..9c4f449 100644 --- a/src/pages/Jobfair/Outdoorfair/Detail/components/FairInfoTab.tsx +++ b/src/pages/Jobfair/Outdoorfair/Detail/components/FairInfoTab.tsx @@ -25,10 +25,11 @@ const OUTDOOR_FAIR_REGION_DICT = 'outdoor_fair_region'; interface Props { fairId: number; fairInfo: OutdoorFairItem; + refreshKey?: number; onFairUpdate: () => void; } -const FairInfoTab: React.FC = ({ fairId, fairInfo, onFairUpdate }) => { +const FairInfoTab: React.FC = ({ fairId, fairInfo, refreshKey, onFairUpdate }) => { const access = useAccess(); const actionRef = useRef(); const [editModalOpen, setEditModalOpen] = useState(false); @@ -64,6 +65,10 @@ const FairInfoTab: React.FC = ({ fairId, fairInfo, onFairUpdate }) => { refreshEditModalOptions(); }, [refreshEditModalOptions]); + useEffect(() => { + actionRef.current?.reload(); + }, [refreshKey]); + const handleCreateDictOption = async (values: OutdoorFairDictForm) => { const res = await addOutdoorFairDictOption(values); if (res.code === 200) { @@ -283,7 +288,7 @@ const FairInfoTab: React.FC = ({ fairId, fairInfo, onFairUpdate }) => { title: '确认移除', content: `确定移除「${record.companyName}」吗?`, onOk: async () => { - const res = await removeParticipatingCompany(record.id); + const res = await removeParticipatingCompany(record.id, fairId); if (res.code === 200) { message.success('移除成功'); actionRef.current?.reload(); diff --git a/src/pages/Jobfair/Outdoorfair/Detail/index.tsx b/src/pages/Jobfair/Outdoorfair/Detail/index.tsx index 9624c46..d237b0e 100644 --- a/src/pages/Jobfair/Outdoorfair/Detail/index.tsx +++ b/src/pages/Jobfair/Outdoorfair/Detail/index.tsx @@ -18,6 +18,7 @@ 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); @@ -48,6 +49,13 @@ const OutdoorFairDetail: React.FC = () => { { key: 'statistics', label: '数据统计' }, ]; + const handleTabChange = (key: string) => { + setActiveTab(key); + if (key === 'fair-info') { + setFairInfoRefreshKey((prev) => prev + 1); + } + }; + return ( { {fairInfo && ( ({ key: tab.key, label: tab.label, @@ -75,6 +83,7 @@ const OutdoorFairDetail: React.FC = () => { ); diff --git a/src/pages/Jobfair/Venueinfo/Detail/index.tsx b/src/pages/Jobfair/Venueinfo/Detail/index.tsx deleted file mode 100644 index 9952068..0000000 --- a/src/pages/Jobfair/Venueinfo/Detail/index.tsx +++ /dev/null @@ -1,402 +0,0 @@ -import React, { useMemo, useState, useEffect } from 'react'; -import { history, useSearchParams } from '@umijs/max'; -import { PageContainer } from '@ant-design/pro-components'; -import { - Button, - Card, - Descriptions, - Input, - InputNumber, - Modal, - Space, - Spin, - Tag, - message, -} from 'antd'; -import { ArrowLeftOutlined } from '@ant-design/icons'; -import { Rnd } from 'react-rnd'; -import { - getVenueBoothList, - getVenueInfo, - saveVenueBoothList, -} from '@/services/jobportal/venueInfo'; -import type { VenueBoothItem, VenueInfoItem } from '@/services/jobportal/venueInfo'; - -const BOOTH_WIDTH = 88; -const BOOTH_HEIGHT = 44; -const BOOTH_GAP_X = 24; -const BOOTH_GAP_Y = 28; -const BOOTH_START_X = 40; -const BOOTH_START_Y = 40; -const BOOTHS_PER_ROW = 10; - -const VenueInfoDetail: React.FC = () => { - const [searchParams] = useSearchParams(); - const venueId = Number(searchParams.get('id')); - const [venueInfo, setVenueInfo] = useState(); - const [booths, setBooths] = useState([]); - const [boothModalOpen, setBoothModalOpen] = useState(false); - const [editingBoothKey, setEditingBoothKey] = useState(); - const [boothNumber, setBoothNumber] = useState(''); - const [generateModalOpen, setGenerateModalOpen] = useState(false); - const [generateRows, setGenerateRows] = useState(6); - const [generateColumns, setGenerateColumns] = useState(10); - const [loading, setLoading] = useState(true); - - const getBoothKey = (booth: VenueBoothItem, index: number) => - String(booth.id || `${booth.boothNumber}-${index}`); - - const canvasSize = useMemo(() => { - const maxX = booths.reduce((max, booth) => Math.max(max, booth.positionX || 0), 0); - const maxY = booths.reduce((max, booth) => Math.max(max, booth.positionY || 0), 0); - return { - width: Math.max(1200, maxX + BOOTH_WIDTH + BOOTH_START_X), - height: Math.max(520, maxY + BOOTH_HEIGHT + BOOTH_START_Y), - }; - }, [booths]); - - const arrangeBooths = (items: VenueBoothItem[], columns = BOOTHS_PER_ROW) => - items.map((booth, index) => ({ - ...booth, - positionX: BOOTH_START_X + (index % columns) * (BOOTH_WIDTH + BOOTH_GAP_X), - positionY: BOOTH_START_Y + Math.floor(index / columns) * (BOOTH_HEIGHT + BOOTH_GAP_Y), - })); - - const hasOverlappedBooths = (items: VenueBoothItem[]) => { - for (let i = 0; i < items.length; i += 1) { - for (let j = i + 1; j < items.length; j += 1) { - const first = items[i]; - const second = items[j]; - const firstX = first.positionX || 0; - const firstY = first.positionY || 0; - const secondX = second.positionX || 0; - const secondY = second.positionY || 0; - const separated = - firstX + BOOTH_WIDTH + 4 <= secondX || - secondX + BOOTH_WIDTH + 4 <= firstX || - firstY + BOOTH_HEIGHT + 4 <= secondY || - secondY + BOOTH_HEIGHT + 4 <= firstY; - if (!separated) { - return true; - } - } - } - return false; - }; - - useEffect(() => { - const fetchVenueInfo = async () => { - if (!venueId) { - setLoading(false); - return; - } - setLoading(true); - try { - const res = await getVenueInfo(venueId); - if (res.code === 200 && res.data) { - setVenueInfo(res.data); - if (res.data.floorCount) { - const columns = Math.min(10, Math.max(1, res.data.floorCount)); - setGenerateColumns(columns); - setGenerateRows(Math.max(1, Math.ceil(res.data.floorCount / columns))); - } - const boothRes = await getVenueBoothList(venueId); - if (boothRes.code === 200) { - const fetchedBooths = boothRes.data || []; - setBooths( - hasOverlappedBooths(fetchedBooths) ? arrangeBooths(fetchedBooths) : fetchedBooths, - ); - } - } else { - message.error(res.msg || '未找到该场地信息'); - } - } finally { - setLoading(false); - } - }; - fetchVenueInfo(); - }, [venueId]); - - const handleGenerateBooths = () => { - const rows = Math.max(1, generateRows || 1); - const columns = Math.max(1, generateColumns || 1); - const count = rows * columns; - const generated = Array.from({ length: count }).map((_, index) => ({ - boothNumber: `KJ${String(index + 1).padStart(2, '0')}`, - positionX: BOOTH_START_X + (index % columns) * (BOOTH_WIDTH + BOOTH_GAP_X), - positionY: BOOTH_START_Y + Math.floor(index / columns) * (BOOTH_HEIGHT + BOOTH_GAP_Y), - })); - setBooths(generated); - setGenerateModalOpen(false); - message.success( - `已生成 ${rows} 行 × ${columns} 列,共 ${count} 个展位,确认后请点击保存展位图`, - ); - }; - - const handleArrangeBooths = () => { - setBooths(arrangeBooths(booths, Math.max(1, generateColumns || BOOTHS_PER_ROW))); - message.success('已整理布局,确认后请点击保存展位图'); - }; - - const openBoothModal = (booth?: VenueBoothItem, index?: number) => { - setEditingBoothKey(booth && index !== undefined ? getBoothKey(booth, index) : undefined); - setBoothNumber(booth?.boothNumber || `KJ${String(booths.length + 1).padStart(2, '0')}`); - setBoothModalOpen(true); - }; - - const handleSaveBoothNumber = () => { - const trimmed = boothNumber.trim(); - if (!trimmed) { - message.warning('请输入展位号'); - return; - } - if ( - booths.some( - (booth, index) => - booth.boothNumber === trimmed && getBoothKey(booth, index) !== editingBoothKey, - ) - ) { - message.warning('展位号不能重复'); - return; - } - if (editingBoothKey) { - setBooths( - booths.map((booth, index) => - getBoothKey(booth, index) === editingBoothKey - ? { ...booth, boothNumber: trimmed } - : booth, - ), - ); - } else { - setBooths([ - ...booths, - { - boothNumber: trimmed, - positionX: 40, - positionY: 40, - }, - ]); - } - setBoothModalOpen(false); - }; - - const handleMoveBooth = (key: string, x: number, y: number) => { - setBooths((prev) => - prev.map((booth, index) => - getBoothKey(booth, index) === key - ? { ...booth, positionX: Math.round(x), positionY: Math.round(y) } - : booth, - ), - ); - }; - - const handleDeleteBooth = (key: string) => { - setBooths(booths.filter((booth, index) => getBoothKey(booth, index) !== key)); - }; - - const handleSaveBooths = async () => { - if (!venueId) { - return; - } - const res = await saveVenueBoothList(venueId, booths); - if (res.code === 200) { - message.success('展位图保存成功'); - const boothRes = await getVenueBoothList(venueId); - if (boothRes.code === 200) { - setBooths(boothRes.data || []); - } - } else { - message.error(res.msg || '展位图保存失败'); - } - }; - - return ( - history.back(), - extra: [ - , - ], - }} - > - - {venueInfo && ( - - - - {venueInfo.venueType} - - - {venueInfo.venueName} - - {venueInfo.venueArea ?? '--'} - {venueInfo.floorCount ?? '--'} - - {venueInfo.contactPhone || '--'} - - - {venueInfo.venueAddress} - - - {venueInfo.routeLineNames || '--'} - - {venueInfo.handleDate || '--'} - {venueInfo.handleOrg || '--'} - {venueInfo.handler || '--'} - - {venueInfo.remark || '--'} - - - - )} - {venueInfo && ( - - - - - - - } - > -
-
- {booths.map((booth, index) => { - const key = getBoothKey(booth, index); - return ( - handleMoveBooth(key, data.x, data.y)} - > - - - ); - })} -
-
-
- 操作说明:拖动蓝色展位块即可调整位置;点击“改名”修改展位号;点击“保存展位图”后写入数据库。 -
-
- )} -
- setGenerateModalOpen(false)} - onOk={handleGenerateBooths} - okText="生成" - > - - - 行数 - setGenerateRows(Number(value) || 1)} - /> - 列数 - setGenerateColumns(Number(value) || 1)} - /> - -
- 将生成 {Math.max(1, generateRows || 1)} 行 × {Math.max(1, generateColumns || 1)} 列,共{' '} - {Math.max(1, generateRows || 1) * Math.max(1, generateColumns || 1)}{' '} - 个展位。生成后需要点击“保存展位图”才会写入数据库。 -
-
-
- setBoothModalOpen(false)} - onOk={handleSaveBoothNumber} - okText="确定" - > - setBoothNumber(event.target.value)} - onPressEnter={handleSaveBoothNumber} - /> - -
- ); -}; - -export default VenueInfoDetail; diff --git a/src/pages/Jobfair/Venueinfo/index.tsx b/src/pages/Jobfair/Venueinfo/index.tsx index 144e964..a116601 100644 --- a/src/pages/Jobfair/Venueinfo/index.tsx +++ b/src/pages/Jobfair/Venueinfo/index.tsx @@ -1,8 +1,8 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { history, useAccess, useModel } from '@umijs/max'; +import { useAccess, useModel } from '@umijs/max'; import { Button, message, Modal } from 'antd'; import { ActionType, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components'; -import { AppstoreOutlined, DeleteOutlined, FormOutlined, PlusOutlined } from '@ant-design/icons'; +import { DeleteOutlined, FormOutlined, PlusOutlined } from '@ant-design/icons'; import { addVenueInfo, addVenueInfoDictOption, @@ -153,19 +153,9 @@ const VenueInfoList: React.FC = () => { { title: '操作', valueType: 'option', - width: 220, + width: 140, fixed: 'right', render: (_, record) => [ - ,