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:
2026-06-25 16:07:43 +08:00
parent 50372da241
commit 9012b0c4a7
9 changed files with 1804 additions and 768 deletions

View File

@@ -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}

View File

@@ -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>
</>
);
};

View File

@@ -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();