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.
This commit is contained in:
@@ -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<Props> = ({ fairId, fairInfo }) => {
|
||||
const access = useAccess();
|
||||
const [booths, setBooths] = useState<VenueBoothItem[]>([]);
|
||||
@@ -26,6 +53,51 @@ const BoothMapTab: React.FC<Props> = ({ fairId, fairInfo }) => {
|
||||
const [bookingModalOpen, setBookingModalOpen] = useState(false);
|
||||
const [companyId, setCompanyId] = useState<number>();
|
||||
const [swapBoothIds, setSwapBoothIds] = useState<number[]>([]);
|
||||
const [boothModalOpen, setBoothModalOpen] = useState(false);
|
||||
const [editingBoothKey, setEditingBoothKey] = useState<string>();
|
||||
const [boothNumber, setBoothNumber] = useState('');
|
||||
const [generateModalOpen, setGenerateModalOpen] = useState(false);
|
||||
const [generateRows, setGenerateRows] = useState(2);
|
||||
const [generateColumns, setGenerateColumns] = useState(10);
|
||||
const [activeBoothKey, setActiveBoothKey] = useState<string>();
|
||||
const [hoveredBoothKey, setHoveredBoothKey] = useState<string>();
|
||||
|
||||
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<Props> = ({ 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<Props> = ({ 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<Props> = ({ fairId, fairInfo }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Row gutter={16}>
|
||||
<Col flex="auto">
|
||||
<Card
|
||||
title={`展位图${fairInfo.venueName ? ` - ${fairInfo.venueName}` : ''}`}
|
||||
loading={loading}
|
||||
extra={
|
||||
<Space>
|
||||
<Row gutter={16} wrap={false} align="top">
|
||||
<Col flex="1 1 0" style={{ minWidth: 0 }}>
|
||||
<Card loading={loading}>
|
||||
<div style={{ marginBottom: 16, textAlign: 'left' }}>
|
||||
<Space wrap size={12}>
|
||||
<Button onClick={() => openBoothModal()}>新增展位</Button>
|
||||
<Button onClick={() => setGenerateModalOpen(true)}>按行列生成</Button>
|
||||
<Button onClick={handleArrangeBooths}>一键整理布局</Button>
|
||||
<Button type="primary" onClick={handleSaveBooths}>
|
||||
保存展位图
|
||||
</Button>
|
||||
<Button
|
||||
icon={<SwapOutlined />}
|
||||
disabled={swapBoothIds.length !== 2}
|
||||
@@ -167,12 +361,11 @@ const BoothMapTab: React.FC<Props> = ({ fairId, fairInfo }) => {
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
</div>
|
||||
{!fairInfo.venueId ? (
|
||||
<Empty description="当前招聘会未绑定场地" />
|
||||
) : booths.length === 0 ? (
|
||||
<Empty description="当前场地还没有维护展位图,请先到场地信息维护里编辑展位图" />
|
||||
<Empty description="当前招聘会还没有展位图,请点击“按行列生成”创建展位图" />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
@@ -183,76 +376,280 @@ const BoothMapTab: React.FC<Props> = ({ 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 (
|
||||
<Tooltip
|
||||
key={boothId}
|
||||
title={reserved ? booth.companyName || '已预定' : '未预定'}
|
||||
placement="top"
|
||||
>
|
||||
<div
|
||||
onClick={() => 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,
|
||||
}}
|
||||
<div
|
||||
style={{
|
||||
width: canvasSize.width,
|
||||
height: canvasSize.height,
|
||||
position: 'relative',
|
||||
backgroundSize: '20px 20px',
|
||||
backgroundImage:
|
||||
'linear-gradient(to right, rgba(0,0,0,0.04) 1px, transparent 1px), linear-gradient(to bottom, rgba(0,0,0,0.04) 1px, transparent 1px)',
|
||||
}}
|
||||
>
|
||||
{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 (
|
||||
<Popover
|
||||
key={key}
|
||||
trigger="click"
|
||||
placement="rightTop"
|
||||
open={activeBoothKey === key}
|
||||
onOpenChange={(open) => setActiveBoothKey(open ? key : undefined)}
|
||||
title={`${booth.boothNumber}:${reserved ? '已预定' : '未预定'}`}
|
||||
content={
|
||||
<Space direction="vertical" size={8} style={{ width: 180 }}>
|
||||
{reserved && (
|
||||
<Tooltip title={booth.companyName}>
|
||||
<div
|
||||
style={{
|
||||
color: '#666',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{booth.companyName || '暂无企业名称'}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Button
|
||||
block
|
||||
type="primary"
|
||||
size="small"
|
||||
hidden={!access.hasPerms('cms:outdoorFair:edit')}
|
||||
onClick={() => {
|
||||
setActiveBoothKey(undefined);
|
||||
openBookingModal(booth);
|
||||
}}
|
||||
>
|
||||
{reserved ? '改单位' : '预定'}
|
||||
</Button>
|
||||
<Button
|
||||
block
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setActiveBoothKey(undefined);
|
||||
openBoothModal(booth, index);
|
||||
}}
|
||||
>
|
||||
改名称
|
||||
</Button>
|
||||
<Button
|
||||
block
|
||||
danger
|
||||
size="small"
|
||||
disabled={reserved}
|
||||
onClick={() => handleDeleteBooth(key)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
{reserved && (
|
||||
<Button block size="small" onClick={() => toggleSwapBooth(booth)}>
|
||||
{swapBoothIds.includes(boothId) ? '取消调换选择' : '选择调换'}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div>{booth.boothNumber}</div>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
style={{ color: '#fff', padding: 0, height: 16, fontSize: 11 }}
|
||||
hidden={!access.hasPerms('cms:outdoorFair:edit')}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openBookingModal(booth);
|
||||
<Rnd
|
||||
bounds="parent"
|
||||
size={{ width: BOOTH_WIDTH, height: BOOTH_HEIGHT }}
|
||||
position={{ x: booth.positionX || 0, y: booth.positionY || 0 }}
|
||||
dragGrid={[8, 8]}
|
||||
enableResizing={false}
|
||||
onDragStop={(_, data) => handleMoveBooth(key, data.x, data.y)}
|
||||
>
|
||||
<div
|
||||
onMouseEnter={() => setHoveredBoothKey(key)}
|
||||
onMouseLeave={() => setHoveredBoothKey(undefined)}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '4px 2px',
|
||||
textAlign: 'center',
|
||||
borderRadius: 4,
|
||||
cursor: 'move',
|
||||
color: '#fff',
|
||||
background: reserved ? '#52c41a' : '#1890ff',
|
||||
outline: selectedForSwap ? '3px solid #fa8c16' : undefined,
|
||||
boxShadow: selectedForSwap ? '0 0 0 2px #fff inset' : undefined,
|
||||
fontSize: 12,
|
||||
userSelect: 'none',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{reserved ? '改单位' : '预定'}
|
||||
</Button>
|
||||
</div>
|
||||
<div>{booth.boothNumber}</div>
|
||||
<div style={{ fontSize: 11 }}>{reserved ? '改单位' : '预定'}</div>
|
||||
</div>
|
||||
</Rnd>
|
||||
</Popover>
|
||||
);
|
||||
})}
|
||||
{booths.map((booth, index) => {
|
||||
const key = getBoothKey(booth, index);
|
||||
const showCompanyName =
|
||||
hoveredBoothKey === key && booth.status === 'reserved' && booth.companyName;
|
||||
if (!showCompanyName) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={`hover-${key}`}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: Math.max(0, (booth.positionX || 0) + BOOTH_WIDTH / 2),
|
||||
top: Math.max(0, (booth.positionY || 0) - 34),
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 1000,
|
||||
maxWidth: 220,
|
||||
padding: '6px 10px',
|
||||
borderRadius: 4,
|
||||
color: '#fff',
|
||||
background: 'rgba(0, 0, 0, 0.75)',
|
||||
fontSize: 12,
|
||||
lineHeight: '18px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
pointerEvents: 'none',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.18)',
|
||||
}}
|
||||
>
|
||||
{booth.companyName}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col flex="300px">
|
||||
<Card title="展位概况">
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={12}>
|
||||
<Tag color="green" style={{ width: '100%', textAlign: 'center', padding: 8 }}>
|
||||
已预定({stats.reserved})
|
||||
</Tag>
|
||||
<Tag color="blue" style={{ width: '100%', textAlign: 'center', padding: 8 }}>
|
||||
未安排({stats.available})
|
||||
</Tag>
|
||||
<Tag style={{ width: '100%', textAlign: 'center', padding: 8 }}>
|
||||
总展位({stats.total})
|
||||
</Tag>
|
||||
</Space>
|
||||
<div style={{ marginTop: 16, color: '#999' }}>
|
||||
提示:点击两个绿色已预定展位后,可点击“调换位置”交换预定单位。
|
||||
</div>
|
||||
<Col flex="320px">
|
||||
<Card>
|
||||
<Tabs
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
key: 'overview',
|
||||
label: '展位概况',
|
||||
children: (
|
||||
<>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={12}>
|
||||
<Tag color="green" style={{ width: '100%', textAlign: 'center', padding: 8 }}>
|
||||
已预定({stats.reserved})
|
||||
</Tag>
|
||||
<Tag color="blue" style={{ width: '100%', textAlign: 'center', padding: 8 }}>
|
||||
未安排({stats.available})
|
||||
</Tag>
|
||||
<Tag style={{ width: '100%', textAlign: 'center', padding: 8 }}>
|
||||
总展位({stats.total})
|
||||
</Tag>
|
||||
</Space>
|
||||
<div style={{ marginTop: 16, color: '#999' }}>
|
||||
提示:拖动展位可调整副本布局;点击两个绿色已预定展位后,可点击“调换位置”交换预定单位。
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'list',
|
||||
label: '展位列表',
|
||||
children:
|
||||
boothList.length === 0 ? (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无展位" />
|
||||
) : (
|
||||
<List
|
||||
size="small"
|
||||
style={{ maxHeight: 520, overflow: 'auto' }}
|
||||
dataSource={boothList}
|
||||
renderItem={(booth) => {
|
||||
const reserved = booth.status === 'reserved';
|
||||
return (
|
||||
<List.Item style={{ paddingInline: 0 }}>
|
||||
<Space direction="vertical" size={2} style={{ width: '100%' }}>
|
||||
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
<Tag color={reserved ? 'green' : 'blue'}>{booth.boothNumber}</Tag>
|
||||
<Tag color={reserved ? 'success' : 'default'}>
|
||||
{reserved ? '已预定' : '未预定'}
|
||||
</Tag>
|
||||
</Space>
|
||||
<Tooltip title={booth.companyName}>
|
||||
<div
|
||||
style={{
|
||||
color: reserved ? '#333' : '#999',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{reserved
|
||||
? booth.companyName || '已预定,暂无企业名称'
|
||||
: '暂无预定企业'}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Modal
|
||||
title="按行列生成展位图"
|
||||
open={generateModalOpen}
|
||||
onCancel={() => setGenerateModalOpen(false)}
|
||||
onOk={handleGenerateBooths}
|
||||
okText="生成"
|
||||
>
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<Space>
|
||||
<span>行数</span>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={50}
|
||||
value={generateRows}
|
||||
onChange={(value) => setGenerateRows(Number(value) || 1)}
|
||||
/>
|
||||
<span>列数</span>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={50}
|
||||
value={generateColumns}
|
||||
onChange={(value) => setGenerateColumns(Number(value) || 1)}
|
||||
/>
|
||||
</Space>
|
||||
<div style={{ color: '#999' }}>
|
||||
将生成 {Math.max(1, generateRows || 1)} 行 × {Math.max(1, generateColumns || 1)} 列,共{' '}
|
||||
{Math.max(1, generateRows || 1) * Math.max(1, generateColumns || 1)}{' '}
|
||||
个展位。生成后需要点击“保存展位图”才会写入招聘会副本。
|
||||
</div>
|
||||
</Space>
|
||||
</Modal>
|
||||
<Modal
|
||||
title={editingBoothKey ? '修改展位号' : '新增展位'}
|
||||
open={boothModalOpen}
|
||||
onCancel={() => setBoothModalOpen(false)}
|
||||
onOk={handleSaveBoothNumber}
|
||||
okText="确定"
|
||||
>
|
||||
<Input
|
||||
value={boothNumber}
|
||||
maxLength={50}
|
||||
placeholder="请输入展位号,如 KJ01"
|
||||
onChange={(event) => setBoothNumber(event.target.value)}
|
||||
onPressEnter={handleSaveBoothNumber}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
title={`预定展位 - ${selectedBooth?.boothNumber || ''}`}
|
||||
open={bookingModalOpen}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Modal, Tag, message } from 'antd';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Empty, Modal, Spin, Tag, Tooltip, message } from 'antd';
|
||||
import { ActionType, ProTable, ProColumns } from '@ant-design/pro-components';
|
||||
import { addParticipatingCompany } from '@/services/jobportal/outdoorFairDetail';
|
||||
import { bookOutdoorFairBooth, getOutdoorFairBoothMap } from '@/services/jobportal/venueInfo';
|
||||
import type { VenueBoothItem } from '@/services/jobportal/venueInfo';
|
||||
import { getCmsCompanyList } from '@/services/company/list';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
import { getCmsIndustryTreeList } from '@/services/classify/industry';
|
||||
@@ -39,6 +41,19 @@ const CompanyAddModal: React.FC<Props> = ({ open, fairId, onCancel, onSuccess })
|
||||
const [selectedRows, setSelectedRows] = useState<API.CompanyList.Company[]>([]);
|
||||
const [scaleEnum, setScaleEnum] = useState<Record<string, any>>({});
|
||||
const [industryEnum, setIndustryEnum] = useState<DictValueEnumObj>({});
|
||||
const [boothModalOpen, setBoothModalOpen] = useState(false);
|
||||
const [boothLoading, setBoothLoading] = useState(false);
|
||||
const [booths, setBooths] = useState<VenueBoothItem[]>([]);
|
||||
const [selectedBoothId, setSelectedBoothId] = useState<number>();
|
||||
|
||||
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<Props> = ({ 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 (
|
||||
<Modal
|
||||
title="添加参会企业"
|
||||
open={open}
|
||||
width={800}
|
||||
destroyOnClose
|
||||
onCancel={() => {
|
||||
setSelectedRows([]);
|
||||
onCancel();
|
||||
}}
|
||||
onOk={handleOk}
|
||||
okText="确认添加"
|
||||
>
|
||||
<ProTable
|
||||
actionRef={actionRef}
|
||||
rowKey="companyId"
|
||||
search={false}
|
||||
options={false}
|
||||
pagination={{ pageSize: 6 }}
|
||||
columns={columns}
|
||||
request={async (params) => {
|
||||
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}
|
||||
/>
|
||||
</Modal>
|
||||
<>
|
||||
<Modal
|
||||
title="添加参会企业"
|
||||
open={open}
|
||||
width={800}
|
||||
destroyOnClose
|
||||
onCancel={resetAndCancel}
|
||||
onOk={handleOk}
|
||||
okText="下一步:选择展位"
|
||||
>
|
||||
<ProTable
|
||||
actionRef={actionRef}
|
||||
rowKey="companyId"
|
||||
search={false}
|
||||
options={false}
|
||||
pagination={{ pageSize: 6 }}
|
||||
columns={columns}
|
||||
request={async (params) => {
|
||||
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}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal
|
||||
title={`选择展位${selectedRows[0]?.name ? ` - ${selectedRows[0].name}` : ''}`}
|
||||
open={boothModalOpen}
|
||||
width={900}
|
||||
destroyOnClose
|
||||
onCancel={() => setBoothModalOpen(false)}
|
||||
onOk={handleConfirmBooth}
|
||||
okText="确认添加并预定"
|
||||
>
|
||||
<Spin spinning={boothLoading}>
|
||||
{booths.length === 0 ? (
|
||||
<Empty description="当前招聘会没有可选展位,请先绑定场地并维护展位图" />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
height: 460,
|
||||
overflow: 'auto',
|
||||
border: '1px solid #f0f0f0',
|
||||
background: '#fafafa',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: canvasSize.width,
|
||||
height: canvasSize.height,
|
||||
}}
|
||||
>
|
||||
{booths.map((booth) => {
|
||||
const boothId = Number(booth.boothId || booth.id);
|
||||
const reserved = booth.status === 'reserved';
|
||||
const selected = selectedBoothId === boothId;
|
||||
return (
|
||||
<Tooltip
|
||||
key={boothId}
|
||||
title={reserved ? `已预定:${booth.companyName || '未知企业'}` : '点击选择'}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
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}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ color: '#999', marginTop: 12 }}>
|
||||
蓝色为空展位,可选择;绿色为已预定展位,不可选择。
|
||||
</div>
|
||||
</Spin>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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<Props> = ({ fairId, fairInfo, onFairUpdate }) => {
|
||||
const FairInfoTab: React.FC<Props> = ({ fairId, fairInfo, refreshKey, onFairUpdate }) => {
|
||||
const access = useAccess();
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
@@ -64,6 +65,10 @@ const FairInfoTab: React.FC<Props> = ({ 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<Props> = ({ 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();
|
||||
|
||||
Reference in New Issue
Block a user