From 50372da241f465211ae465c0b74a31b773b75e4d Mon Sep 17 00:00:00 2001 From: lapuda <577732344@qq.com> Date: Thu, 25 Jun 2026 14:42:14 +0800 Subject: [PATCH] feat: enhance booth management functionality in outdoor fair - Added `react-rnd` for draggable booth positioning in the booth management interface. - Implemented booth booking, cancellation, and swapping features. - Updated `BoothTab` component to fetch and display booth data with enhanced UI for booth interactions. - Introduced modal dialogs for booking and managing booth assignments. - Enhanced venue information service to support booth-related API calls. - Updated `VenueInfoDetail` to allow for booth layout management and generation. - Improved overall user experience with better state management and loading indicators. --- package-lock.json | 57 ++- package.json | 3 +- .../Detail/components/BoothTab.tsx | 460 +++++++++++------- .../Jobfair/Outdoorfair/Detail/index.tsx | 4 +- src/pages/Jobfair/Venueinfo/Detail/index.tsx | 327 ++++++++++++- src/pages/Jobfair/Venueinfo/index.tsx | 16 +- src/services/jobportal/venueInfo.ts | 61 +++ 7 files changed, 735 insertions(+), 193 deletions(-) diff --git a/package-lock.json b/package-lock.json index da72891..7db37b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,8 @@ "react-dev-inspector": "^2.0.1", "react-dom": "^18.3.0", "react-helmet-async": "^2.0.0", - "react-highlight": "^0.15.0" + "react-highlight": "^0.15.0", + "react-rnd": "^10.5.3" }, "devDependencies": { "@ant-design/pro-cli": "^3.3.0", @@ -18142,6 +18143,15 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/cmd-shim": { "version": "5.0.0", "resolved": "https://registry.npmmirror.com/cmd-shim/-/cmd-shim-5.0.0.tgz", @@ -33669,6 +33679,16 @@ "react-dom": ">=16.9.0" } }, + "node_modules/re-resizable": { + "version": "6.11.2", + "resolved": "https://registry.npmmirror.com/re-resizable/-/re-resizable-6.11.2.tgz", + "integrity": "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz", @@ -33782,6 +33802,20 @@ "react": "^18.3.1" } }, + "node_modules/react-draggable": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/react-draggable/-/react-draggable-4.7.0.tgz", + "integrity": "sha512-kTpANmKWVnFXiZ76Ag2ZowiFStuBYnJ606PI1TbUsOg29/400/JNIxI9+CuenhiAqFuXWJffz6F4UI3R51kUug==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-error-overlay": { "version": "6.1.0", "resolved": "https://registry.npmmirror.com/react-error-overlay/-/react-error-overlay-6.1.0.tgz", @@ -33905,6 +33939,27 @@ "node": ">=0.10.0" } }, + "node_modules/react-rnd": { + "version": "10.5.3", + "resolved": "https://registry.npmmirror.com/react-rnd/-/react-rnd-10.5.3.tgz", + "integrity": "sha512-s/sIT3pGZnQ+57egijkTp9mizjIWrJz68Pq6yd+F/wniFY3IriML18dUXnQe/HP9uMiJ+9MAp44hljG99fZu6Q==", + "license": "MIT", + "dependencies": { + "re-resizable": "^6.11.2", + "react-draggable": "^4.5.0", + "tslib": "2.6.2" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, + "node_modules/react-rnd/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, "node_modules/react-router": { "version": "6.3.0", "resolved": "https://registry.npmmirror.com/react-router/-/react-router-6.3.0.tgz", diff --git a/package.json b/package.json index 2910a57..07e83ac 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,8 @@ "react-dev-inspector": "^2.0.1", "react-dom": "^18.3.0", "react-helmet-async": "^2.0.0", - "react-highlight": "^0.15.0" + "react-highlight": "^0.15.0", + "react-rnd": "^10.5.3" }, "devDependencies": { "@ant-design/pro-cli": "^3.3.0", diff --git a/src/pages/Jobfair/Outdoorfair/Detail/components/BoothTab.tsx b/src/pages/Jobfair/Outdoorfair/Detail/components/BoothTab.tsx index 8fddc36..ec27231 100644 --- a/src/pages/Jobfair/Outdoorfair/Detail/components/BoothTab.tsx +++ b/src/pages/Jobfair/Outdoorfair/Detail/components/BoothTab.tsx @@ -1,200 +1,296 @@ -import React, { useRef, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useAccess } from '@umijs/max'; -import { Button, message, Modal, Tag, Dropdown } from 'antd'; -import { ActionType, ProColumns, ProTable, ModalForm, ProFormText, ProFormSelect } from '@ant-design/pro-components'; -import { PlusOutlined, DeleteOutlined, FormOutlined, CheckOutlined, CloseOutlined, SendOutlined, EllipsisOutlined, ApartmentOutlined } from '@ant-design/icons'; +import { Button, Card, Col, Empty, Modal, Row, Select, Space, Tag, Tooltip, message } from 'antd'; +import { ReloadOutlined, SwapOutlined } from '@ant-design/icons'; +import { getCmsCompanyList } from '@/services/company/list'; import { - getBoothList, - addBooth, - updateBooth, - deleteBooth, - reviewBooth, - assignBooth, -} from '@/services/jobportal/outdoorFairDetail'; -import { getCompanyRegistrationList } from '@/services/jobportal/outdoorFairDetail'; + bookOutdoorFairBooth, + cancelOutdoorFairBooth, + getOutdoorFairBoothMap, + swapOutdoorFairBooth, +} from '@/services/jobportal/venueInfo'; +import type { VenueBoothItem } from '@/services/jobportal/venueInfo'; +import type { OutdoorFairItem } from '@/services/jobportal/outdoorFair'; interface Props { fairId: number; + fairInfo: OutdoorFairItem; } -const reviewStatusMap: Record = { - pending: { text: '待审核', color: 'default' }, - approved: { text: '已通过', color: 'green' }, - rejected: { text: '已拒绝', color: 'red' }, -}; - -const BoothTab: React.FC = ({ fairId }) => { +const BoothMapTab: React.FC = ({ fairId, fairInfo }) => { const access = useAccess(); - const actionRef = useRef(); - const [modalOpen, setModalOpen] = useState(false); - const [editingBooth, setEditingBooth] = useState(null); - const [assignModalOpen, setAssignModalOpen] = useState(false); - const [assigningBooth, setAssigningBooth] = useState(null); + const [booths, setBooths] = useState([]); + const [companies, setCompanies] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedBooth, setSelectedBooth] = useState(); + const [bookingModalOpen, setBookingModalOpen] = useState(false); + const [companyId, setCompanyId] = useState(); + const [swapBoothIds, setSwapBoothIds] = useState([]); - const columns: ProColumns[] = [ - { - title: '展位号', - dataIndex: 'boothNumber', - width: 100, - hideInTable: true, - fieldProps: { placeholder: '请输入展位号' }, - }, - { - title: '审核状态', - dataIndex: 'reviewStatus', - width: 100, - hideInTable: true, - valueType: 'select', - options: [ - { label: '待审核', value: 'pending' }, - { label: '已通过', value: 'approved' }, - { label: '已拒绝', value: 'rejected' }, - ], - }, - { title: '展位号', dataIndex: 'boothNumber', width: 100, hideInSearch: true }, - { title: '企业名称', dataIndex: 'companyName', ellipsis: true, hideInSearch: true, render: (_, r) => r.companyName || '--' }, - { title: '岗位数', dataIndex: 'jobCount', width: 80, hideInSearch: true }, - { title: '审核状态', dataIndex: 'reviewStatus', width: 100, - render: (_, r) => { - const s = reviewStatusMap[r.reviewStatus] || { text: r.reviewStatus, color: 'default' }; - return {s.text}; - }, - }, - { title: '分配时间', dataIndex: 'assignedTime', valueType: 'dateTime', width: 170, hideInSearch: true, render: (_, r) => r.assignedTime || '--' }, - { - title: '操作', valueType: 'option', width: 150, - render: (_, record) => { - const menuItems = [ - { - key: 'assign', - icon: , - label: '分配展位', - onClick: () => { setAssigningBooth(record); setAssignModalOpen(true); }, - }, - { - key: 'edit', - icon: , - label: '编辑', - onClick: () => { setEditingBooth(record); setModalOpen(true); }, - }, - { - key: 'notify', - icon: , - label: '发送通知', - disabled: !record.companyName, - onClick: () => { - if (!record.companyName) { message.warning('该展位尚未分配企业'); return; } - message.success(`已向「${record.companyName}」发送展位信息通知`); - }, - }, - { type: 'divider' as const }, - { - key: 'delete', - icon: , - label: '删除', - danger: true, - onClick: () => { - Modal.confirm({ - title: '确认删除', content: `确定删除展位「${record.boothNumber}」吗?`, - onOk: async () => { - const res = await deleteBooth(record.id); - if (res.code === 200) { message.success('删除成功'); actionRef.current?.reload(); } - }, - }); - }, - }, - ]; - return [ - record.reviewStatus === 'pending' && ( - - ), - record.reviewStatus === 'pending' && ( - - ), - - , - ]} - /> - - {/* 新增/编辑展位 */} - setModalOpen(false) }} - onFinish={async (values) => { - const res = editingBooth - ? await updateBooth({ id: editingBooth.id, ...values }) - : await addBooth({ fairId, ...values }); - if (res.code === 200) { message.success(res.msg); setModalOpen(false); actionRef.current?.reload(); return true; } - message.error(res.msg); return false; - }} - initialValues={editingBooth} + + + + + + + } + > + {!fairInfo.venueId ? ( + + ) : booths.length === 0 ? ( + + ) : ( +
+ {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, + }} + > +
{booth.boothNumber}
+
+ +
+
+
+ ); + })} +
+ )} +
+ + + + + + 已预定({stats.reserved}) + + + 未安排({stats.available}) + + + 总展位({stats.total}) + + +
+ 提示:点击两个绿色已预定展位后,可点击“调换位置”交换预定单位。 +
+
+ + setBookingModalOpen(false)} + onOk={handleBookBooth} + okText="确认预定" + footer={(_, { OkBtn, CancelBtn }) => ( + + {selectedBooth?.status === 'reserved' && ( + + )} + + + + )} > - -
- - {/* 分配展位给企业 */} - setAssignModalOpen(false) }} - onFinish={async (values) => { - if (!assigningBooth) return false; - const res = await assignBooth({ - fairId, boothId: assigningBooth.id, companyId: values.companyId, boothNumber: assigningBooth.boothNumber, - }); - if (res.code === 200) { message.success(res.msg); setAssignModalOpen(false); actionRef.current?.reload(); return true; } - message.error(res.msg); return false; - }} - > - { - const res = await getCompanyRegistrationList(fairId, { reviewStatus: 'approved' }); - return res.rows.map((r: any) => ({ label: r.companyName, value: r.companyId })); - }} - placeholder="请搜索并选择企业" + setBoothNumber(event.target.value)} + onPressEnter={handleSaveBoothNumber} + /> + ); }; diff --git a/src/pages/Jobfair/Venueinfo/index.tsx b/src/pages/Jobfair/Venueinfo/index.tsx index a116601..144e964 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 { useAccess, useModel } from '@umijs/max'; +import { history, useAccess, useModel } from '@umijs/max'; import { Button, message, Modal } from 'antd'; import { ActionType, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components'; -import { DeleteOutlined, FormOutlined, PlusOutlined } from '@ant-design/icons'; +import { AppstoreOutlined, DeleteOutlined, FormOutlined, PlusOutlined } from '@ant-design/icons'; import { addVenueInfo, addVenueInfoDictOption, @@ -153,9 +153,19 @@ const VenueInfoList: React.FC = () => { { title: '操作', valueType: 'option', - width: 140, + width: 220, fixed: 'right', render: (_, record) => [ + ,