feat: manage venue booth templates in job fair admin
Some checks failed
Node CI / build (14.x, macOS-latest) (push) Has been cancelled
Node CI / build (14.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (14.x, windows-latest) (push) Has been cancelled
Node CI / build (16.x, macOS-latest) (push) Has been cancelled
Node CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (16.x, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
coverage CI / build (push) Has been cancelled
Node pnpm CI / build (16.x, macOS-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, windows-latest) (push) Has been cancelled
Some checks failed
Node CI / build (14.x, macOS-latest) (push) Has been cancelled
Node CI / build (14.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (14.x, windows-latest) (push) Has been cancelled
Node CI / build (16.x, macOS-latest) (push) Has been cancelled
Node CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (16.x, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
coverage CI / build (push) Has been cancelled
Node pnpm CI / build (16.x, macOS-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, windows-latest) (push) Has been cancelled
This commit is contained in:
@@ -147,7 +147,30 @@ const EditModal: React.FC<EditModalProps> = ({
|
|||||||
width={700}
|
width={700}
|
||||||
modalProps={{ destroyOnClose: true, onCancel }}
|
modalProps={{ destroyOnClose: true, onCancel }}
|
||||||
onFinish={async (formValues) => {
|
onFinish={async (formValues) => {
|
||||||
await onSubmit({ ...formValues, id: values?.id } as OutdoorFairForm);
|
const payload = { ...formValues, id: values?.id } as OutdoorFairForm;
|
||||||
|
const venueChanged =
|
||||||
|
!!values?.id &&
|
||||||
|
values.venueId !== undefined &&
|
||||||
|
Number(values.venueId) !== Number(payload.venueId);
|
||||||
|
if (venueChanged && (values?.boothCount || 0) > 0) {
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认更换场地并重建展位图?',
|
||||||
|
content:
|
||||||
|
'当前招聘会已有展位图。确认后将删除原展位图和所有企业预定,再按新场地的展位模板重新生成;此操作不可恢复。',
|
||||||
|
okText: '确认重建',
|
||||||
|
okButtonProps: { danger: true },
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: async () => {
|
||||||
|
await onSubmit({ ...payload, resetBoothMap: true });
|
||||||
|
resolve(true);
|
||||||
|
},
|
||||||
|
onCancel: () => resolve(false),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await onSubmit(payload);
|
||||||
|
return true;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ProFormText name="id" hidden />
|
<ProFormText name="id" hidden />
|
||||||
|
|||||||
@@ -286,6 +286,12 @@ const OutdoorFairList: React.FC = () => {
|
|||||||
dataIndex: 'boothCount',
|
dataIndex: 'boothCount',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '已预定摊位数',
|
||||||
|
dataIndex: 'reservedBoothCount',
|
||||||
|
hideInSearch: true,
|
||||||
|
render: (_, record) => record.reservedBoothCount ?? 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '举办时间',
|
title: '举办时间',
|
||||||
dataIndex: 'holdTime',
|
dataIndex: 'holdTime',
|
||||||
|
|||||||
421
src/pages/Jobfair/Venueinfo/components/VenueBoothMapModal.tsx
Normal file
421
src/pages/Jobfair/Venueinfo/components/VenueBoothMapModal.tsx
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Col,
|
||||||
|
Empty,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
List,
|
||||||
|
Modal,
|
||||||
|
Popover,
|
||||||
|
Row,
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
message,
|
||||||
|
} from 'antd';
|
||||||
|
import { ReloadOutlined } from '@ant-design/icons';
|
||||||
|
import { Rnd } from 'react-rnd';
|
||||||
|
import { getVenueBoothMap, saveVenueBoothMap } from '@/services/jobportal/venueInfo';
|
||||||
|
import type { VenueBoothItem, VenueInfoItem } from '@/services/jobportal/venueInfo';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
venue?: VenueInfoItem;
|
||||||
|
onCancel: () => void;
|
||||||
|
onSaved: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 VenueBoothMapModal: React.FC<Props> = ({ open, venue, onCancel, onSaved }) => {
|
||||||
|
const [booths, setBooths] = useState<VenueBoothItem[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [activeBoothKey, setActiveBoothKey] = useState<string>();
|
||||||
|
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 getBoothKey = (booth: VenueBoothItem, index: number) =>
|
||||||
|
String(booth.id || booth.boothId || `${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 boothList = useMemo(
|
||||||
|
() =>
|
||||||
|
[...booths].sort((a, b) =>
|
||||||
|
String(a.boothNumber || '').localeCompare(String(b.boothNumber || ''), 'zh-CN', {
|
||||||
|
numeric: true,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
[booths],
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
if (!venue?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await getVenueBoothMap(venue.id);
|
||||||
|
if (res.code === 200) {
|
||||||
|
const fetchedBooths = res.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)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(res.msg || '场地展位图加载失败');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setActiveBoothKey(undefined);
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
}, [open, venue?.id]);
|
||||||
|
|
||||||
|
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 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((prev) =>
|
||||||
|
prev.map((booth, index) =>
|
||||||
|
getBoothKey(booth, index) === editingBoothKey
|
||||||
|
? { ...booth, boothNumber: trimmed }
|
||||||
|
: booth,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const nextPosition = getNextBoothPosition();
|
||||||
|
setBooths((prev) => [...prev, { boothNumber: trimmed, ...nextPosition }]);
|
||||||
|
}
|
||||||
|
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((prev) => prev.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 generatedNumber = `KJ${String(index + 1).padStart(2, '0')}`;
|
||||||
|
return {
|
||||||
|
...existingByNumber.get(generatedNumber),
|
||||||
|
boothNumber: generatedNumber,
|
||||||
|
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 = () => {
|
||||||
|
const columns = Math.max(1, generateColumns || BOOTHS_PER_ROW);
|
||||||
|
setBooths((prev) =>
|
||||||
|
prev.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),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
message.success('已整理布局;请点击保存场地模板');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!venue?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const invalid = booths.find((booth) => !booth.boothNumber?.trim());
|
||||||
|
if (invalid) {
|
||||||
|
message.warning('展位号不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const res = await saveVenueBoothMap(
|
||||||
|
venue.id,
|
||||||
|
booths.map((booth) => ({
|
||||||
|
id: booth.id,
|
||||||
|
boothNumber: booth.boothNumber.trim(),
|
||||||
|
positionX: booth.positionX || 0,
|
||||||
|
positionY: booth.positionY || 0,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
if (res.code === 200) {
|
||||||
|
message.success('场地展位模板保存成功');
|
||||||
|
await fetchData();
|
||||||
|
onSaved();
|
||||||
|
} else {
|
||||||
|
message.error(res.msg || '场地展位模板保存失败');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={venue ? `${venue.venueName} - 展位管理` : '展位管理'}
|
||||||
|
open={open}
|
||||||
|
onCancel={onCancel}
|
||||||
|
width={1240}
|
||||||
|
destroyOnClose
|
||||||
|
footer={null}
|
||||||
|
>
|
||||||
|
<Row gutter={16} wrap={false} align="top">
|
||||||
|
<Col flex="1 1 0" style={{ minWidth: 0 }}>
|
||||||
|
<Card loading={loading} bodyStyle={{ padding: 16 }}>
|
||||||
|
<Space wrap size={12} style={{ marginBottom: 16 }}>
|
||||||
|
<Button onClick={() => openBoothModal()}>新增展位</Button>
|
||||||
|
<Button onClick={() => setGenerateModalOpen(true)}>按行列生成</Button>
|
||||||
|
<Button onClick={handleArrangeBooths}>一键整理布局</Button>
|
||||||
|
<Button type="primary" loading={saving} onClick={handleSave}>
|
||||||
|
保存场地模板
|
||||||
|
</Button>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={fetchData}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
{booths.length === 0 ? (
|
||||||
|
<Empty description="当前场地还没有展位模板,请点击“按行列生成”或“新增展位”创建" />
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
minHeight: 520,
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'auto',
|
||||||
|
background: '#fafafa',
|
||||||
|
border: '1px solid #f0f0f0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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 key = getBoothKey(booth, index);
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
key={key}
|
||||||
|
trigger="click"
|
||||||
|
placement="rightTop"
|
||||||
|
open={activeBoothKey === key}
|
||||||
|
onOpenChange={(visible) => setActiveBoothKey(visible ? key : undefined)}
|
||||||
|
title={`${booth.boothNumber}:场地模板展位`}
|
||||||
|
content={
|
||||||
|
<Space direction="vertical" size={8} style={{ width: 160 }}>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setActiveBoothKey(undefined);
|
||||||
|
openBoothModal(booth, index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
改名称
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleDeleteBooth(key)}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
padding: '4px 2px',
|
||||||
|
textAlign: 'center',
|
||||||
|
borderRadius: 4,
|
||||||
|
cursor: 'move',
|
||||||
|
color: '#fff',
|
||||||
|
background: '#1890ff',
|
||||||
|
fontSize: 12,
|
||||||
|
userSelect: 'none',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{booth.boothNumber}
|
||||||
|
</div>
|
||||||
|
</Rnd>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col flex="260px">
|
||||||
|
<Card title="模板概况" size="small">
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size={12}>
|
||||||
|
<Tag color="blue" style={{ width: '100%', textAlign: 'center', padding: 8 }}>
|
||||||
|
模板展位({booths.length})
|
||||||
|
</Tag>
|
||||||
|
<div style={{ color: '#999', fontSize: 12, lineHeight: '20px' }}>
|
||||||
|
此处只维护场地布局。创建户外招聘会并绑定该场地时,系统会复制当前模板为招聘会独立展位图,后续双方互不影响。
|
||||||
|
</div>
|
||||||
|
<List
|
||||||
|
size="small"
|
||||||
|
style={{ maxHeight: 420, overflow: 'auto' }}
|
||||||
|
dataSource={boothList}
|
||||||
|
locale={{ emptyText: '暂无模板展位' }}
|
||||||
|
renderItem={(booth) => <List.Item>{booth.boothNumber}</List.Item>}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VenueBoothMapModal;
|
||||||
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|||||||
import { useAccess, useModel } from '@umijs/max';
|
import { useAccess, useModel } from '@umijs/max';
|
||||||
import { Button, message, Modal } from 'antd';
|
import { Button, message, Modal } from 'antd';
|
||||||
import { ActionType, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
|
import { ActionType, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
|
||||||
import { DeleteOutlined, FormOutlined, PlusOutlined } from '@ant-design/icons';
|
import { ApartmentOutlined, DeleteOutlined, FormOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
addVenueInfo,
|
addVenueInfo,
|
||||||
addVenueInfoDictOption,
|
addVenueInfoDictOption,
|
||||||
@@ -19,6 +19,7 @@ import type {
|
|||||||
import { getCmsLineList } from '@/services/area/subway';
|
import { getCmsLineList } from '@/services/area/subway';
|
||||||
import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict';
|
import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict';
|
||||||
import EditModal from './components/EditModal';
|
import EditModal from './components/EditModal';
|
||||||
|
import VenueBoothMapModal from './components/VenueBoothMapModal';
|
||||||
|
|
||||||
const VENUE_TYPE_DICT = 'venue_info_type';
|
const VENUE_TYPE_DICT = 'venue_info_type';
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ const VenueInfoList: React.FC = () => {
|
|||||||
const actionRef = useRef<ActionType>();
|
const actionRef = useRef<ActionType>();
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const [currentRow, setCurrentRow] = useState<VenueInfoItem>();
|
const [currentRow, setCurrentRow] = useState<VenueInfoItem>();
|
||||||
|
const [templateVenue, setTemplateVenue] = useState<VenueInfoItem>();
|
||||||
const [venueTypeValueEnum, setVenueTypeValueEnum] = useState<Record<string, any>>({});
|
const [venueTypeValueEnum, setVenueTypeValueEnum] = useState<Record<string, any>>({});
|
||||||
const [venueTypeOptions, setVenueTypeOptions] = useState<any[]>([]);
|
const [venueTypeOptions, setVenueTypeOptions] = useState<any[]>([]);
|
||||||
const [lineOptions, setLineOptions] = useState<any[]>([]);
|
const [lineOptions, setLineOptions] = useState<any[]>([]);
|
||||||
@@ -153,9 +155,19 @@ const VenueInfoList: React.FC = () => {
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
valueType: 'option',
|
valueType: 'option',
|
||||||
width: 140,
|
width: 220,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
render: (_, record) => [
|
render: (_, record) => [
|
||||||
|
<Button
|
||||||
|
key="booths"
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
icon={<ApartmentOutlined />}
|
||||||
|
hidden={!access.hasPerms('cms:venueInfo:edit')}
|
||||||
|
onClick={() => setTemplateVenue(record)}
|
||||||
|
>
|
||||||
|
展位管理
|
||||||
|
</Button>,
|
||||||
<Button
|
<Button
|
||||||
key="edit"
|
key="edit"
|
||||||
type="link"
|
type="link"
|
||||||
@@ -240,6 +252,12 @@ const VenueInfoList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<VenueBoothMapModal
|
||||||
|
open={!!templateVenue}
|
||||||
|
venue={templateVenue}
|
||||||
|
onCancel={() => setTemplateVenue(undefined)}
|
||||||
|
onSaved={() => actionRef.current?.reload()}
|
||||||
|
/>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export interface OutdoorFairItem {
|
|||||||
address: string;
|
address: string;
|
||||||
/** 展位数量 */
|
/** 展位数量 */
|
||||||
boothCount: number;
|
boothCount: number;
|
||||||
|
/** 已预定摊位数 */
|
||||||
|
reservedBoothCount?: number;
|
||||||
/** 举办时间 */
|
/** 举办时间 */
|
||||||
holdTime: string;
|
holdTime: string;
|
||||||
/** 截止时间 */
|
/** 截止时间 */
|
||||||
@@ -86,6 +88,8 @@ export interface OutdoorFairForm {
|
|||||||
applyEndTime: string;
|
applyEndTime: string;
|
||||||
onlineApply: boolean;
|
onlineApply: boolean;
|
||||||
photoUrl: string;
|
photoUrl: string;
|
||||||
|
/** 更换场地时确认清空旧展位图与预定,并按新模板重建 */
|
||||||
|
resetBoothMap?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 通用响应 */
|
/** 通用响应 */
|
||||||
|
|||||||
@@ -170,6 +170,22 @@ export async function addVenueInfoDictOption(data: VenueInfoDictForm) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 查询场地的展位图模板。模板不含企业及预定状态。 */
|
||||||
|
export async function getVenueBoothMap(venueId: number) {
|
||||||
|
return request<{ code: number; msg?: string; data: VenueBoothItem[] }>(
|
||||||
|
`${CMS_VENUE_INFO_BASE}/${venueId}/booth-map`,
|
||||||
|
{ method: 'GET' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 完整覆盖保存场地的展位图模板。 */
|
||||||
|
export async function saveVenueBoothMap(venueId: number, data: VenueBoothItem[]) {
|
||||||
|
return request<VenueInfoCommonResult>(`${CMS_VENUE_INFO_BASE}/${venueId}/booths`, {
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function getOutdoorFairBoothMap(fairId: number) {
|
export async function getOutdoorFairBoothMap(fairId: number) {
|
||||||
return request<{ code: number; msg?: string; data: VenueBoothItem[] }>(
|
return request<{ code: number; msg?: string; data: VenueBoothItem[] }>(
|
||||||
`${CMS_VENUE_INFO_BASE}/fair/${fairId}/booth-map`,
|
`${CMS_VENUE_INFO_BASE}/fair/${fairId}/booth-map`,
|
||||||
|
|||||||
Reference in New Issue
Block a user