feat: 增加招聘会机构管理和场地机构选择

This commit is contained in:
2026-07-24 09:42:37 +08:00
parent 5213215c65
commit c6d665c370
7 changed files with 426 additions and 22 deletions

View File

@@ -301,6 +301,11 @@ export default [
path: '/jobfair/venue-info',
component: './Jobfair/Venueinfo',
},
{
name: '机构管理',
path: '/jobfair/institution',
component: './Jobfair/Institution',
},
{
name: '场地信息详情',
path: '/jobfair/venue-info/detail',

View File

@@ -0,0 +1,172 @@
import React, { useRef, useState } from 'react';
import { useAccess } from '@umijs/max';
import { Button, message, Modal } from 'antd';
import {
ActionType,
ModalForm,
PageContainer,
ProColumns,
ProFormText,
ProFormTextArea,
ProTable,
} from '@ant-design/pro-components';
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
import {
addJobFairInstitution,
deleteJobFairInstitution,
getJobFairInstitutionList,
updateJobFairInstitution,
} from '@/services/jobportal/jobFairInstitution';
import type {
JobFairInstitutionForm,
JobFairInstitutionItem,
} from '@/services/jobportal/jobFairInstitution';
const InstitutionManagement: React.FC = () => {
const access = useAccess();
const actionRef = useRef<ActionType>();
const [formOpen, setFormOpen] = useState(false);
const [currentRow, setCurrentRow] = useState<JobFairInstitutionItem>();
const handleDelete = (record: JobFairInstitutionItem) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除机构「${record.institutionName}」吗?如果该机构已被场地使用,需要先解除场地关联。`,
onOk: async () => {
const res = await deleteJobFairInstitution(record.id);
if (res.code === 200) {
message.success('删除成功');
actionRef.current?.reload();
} else {
message.error(res.msg || '删除失败');
}
},
});
};
const columns: ProColumns<JobFairInstitutionItem>[] = [
{
title: '机构名称',
dataIndex: 'institutionName',
ellipsis: true,
width: 260,
},
{
title: '机构简介',
dataIndex: 'institutionIntro',
ellipsis: true,
hideInSearch: true,
},
{
title: '创建时间',
dataIndex: 'createTime',
valueType: 'dateTime',
hideInSearch: true,
width: 180,
},
{
title: '操作',
valueType: 'option',
width: 160,
fixed: 'right',
render: (_, record) => [
<Button
key="edit"
type="link"
size="small"
icon={<EditOutlined />}
hidden={!access.hasPerms('cms:jobFairInstitution:edit')}
onClick={() => {
setCurrentRow(record);
setFormOpen(true);
}}
>
</Button>,
<Button
key="delete"
type="link"
size="small"
danger
icon={<DeleteOutlined />}
hidden={!access.hasPerms('cms:jobFairInstitution:remove')}
onClick={() => handleDelete(record)}
>
</Button>,
],
},
];
return (
<PageContainer>
<ProTable<JobFairInstitutionItem>
headerTitle="机构列表"
actionRef={actionRef}
rowKey="id"
columns={columns}
search={{ labelWidth: 'auto' }}
scroll={{ x: 'max-content' }}
request={async (params) => {
const res = await getJobFairInstitutionList({
institutionName: params.institutionName,
current: params.current,
pageSize: params.pageSize,
});
return { data: res.rows || [], total: res.total || 0, success: res.code === 200 };
}}
toolBarRender={() => [
<Button
key="add"
type="primary"
icon={<PlusOutlined />}
hidden={!access.hasPerms('cms:jobFairInstitution:add')}
onClick={() => {
setCurrentRow(undefined);
setFormOpen(true);
}}
>
</Button>,
]}
/>
<ModalForm<JobFairInstitutionForm>
title={currentRow ? '编辑机构' : '新增机构'}
open={formOpen}
initialValues={currentRow}
modalProps={{ destroyOnClose: true, onCancel: () => setFormOpen(false) }}
onFinish={async (values) => {
const res = currentRow
? await updateJobFairInstitution({ ...values, id: currentRow.id })
: await addJobFairInstitution(values);
if (res.code === 200) {
message.success(currentRow ? '修改成功' : '新增成功');
setFormOpen(false);
setCurrentRow(undefined);
actionRef.current?.reload();
return true;
}
message.error(res.msg || '操作失败');
return false;
}}
>
<ProFormText
name="institutionName"
label="机构名称"
rules={[{ required: true, message: '请输入机构名称' }]}
fieldProps={{ maxLength: 200, showCount: true }}
placeholder="请输入机构名称"
/>
<ProFormTextArea
name="institutionIntro"
label="机构简介"
fieldProps={{ maxLength: 2000, showCount: true, rows: 5 }}
placeholder="请输入机构简介"
/>
</ModalForm>
</PageContainer>
);
};
export default InstitutionManagement;

View File

@@ -8,9 +8,21 @@ import {
getVenueInfo,
updateVenueInfo,
} from '@/services/jobportal/venueInfo';
import type { VenueInfoDictForm, VenueInfoForm, VenueInfoItem } from '@/services/jobportal/venueInfo';
import type {
VenueInfoDictForm,
VenueInfoForm,
VenueInfoItem,
} from '@/services/jobportal/venueInfo';
import { getCmsLineList } from '@/services/area/subway';
import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict';
import {
addJobFairInstitution,
getJobFairInstitutionList,
} from '@/services/jobportal/jobFairInstitution';
import type {
JobFairInstitutionForm,
JobFairInstitutionItem,
} from '@/services/jobportal/jobFairInstitution';
import EditModal from '../components/EditModal';
const VENUE_TYPE_DICT = 'venue_info_type';
@@ -25,6 +37,7 @@ const VenueInfoDetail: React.FC = () => {
const [venueTypeOptions, setVenueTypeOptions] = useState<any[]>([]);
const [venueTypeValueEnum, setVenueTypeValueEnum] = useState<Record<string, any>>({});
const [lineOptions, setLineOptions] = useState<any[]>([]);
const [institutionOptions, setInstitutionOptions] = useState<JobFairInstitutionItem[]>([]);
const fetchVenueInfo = useCallback(async () => {
if (!venueId) {
@@ -45,10 +58,11 @@ const VenueInfoDetail: React.FC = () => {
}, [venueId]);
const refreshOptions = useCallback(async () => {
const [valueEnum, options, lineRes] = await Promise.all([
const [valueEnum, options, lineRes, institutionRes] = await Promise.all([
getDictValueEnum(VENUE_TYPE_DICT),
getDictSelectOption(VENUE_TYPE_DICT),
getCmsLineList({ current: '1', pageSize: '999' } as any),
getJobFairInstitutionList({ current: 1, pageSize: 9999 }),
]);
setVenueTypeValueEnum(valueEnum);
setVenueTypeOptions(options);
@@ -60,8 +74,23 @@ const VenueInfoDetail: React.FC = () => {
})),
);
}
if (institutionRes.code === 200) {
setInstitutionOptions(institutionRes.rows || []);
}
}, []);
const handleCreateInstitution = async (values: JobFairInstitutionForm) => {
const res = await addJobFairInstitution(values);
if (res.code === 200 && res.data) {
const listRes = await getJobFairInstitutionList({ current: 1, pageSize: 9999 });
if (listRes.code === 200) setInstitutionOptions(listRes.rows || []);
message.success('机构新增成功');
return res.data;
}
message.error(res.msg || '机构新增失败');
return undefined;
};
useEffect(() => {
fetchVenueInfo();
refreshOptions();
@@ -80,11 +109,7 @@ const VenueInfoDetail: React.FC = () => {
const venueTypeLabel = (val?: string) => {
if (!val) return '--';
return (
venueTypeValueEnum[val]?.text ||
venueTypeValueEnum[val]?.label ||
val
);
return venueTypeValueEnum[val]?.text || venueTypeValueEnum[val]?.label || val;
};
return (
@@ -120,27 +145,20 @@ const VenueInfoDetail: React.FC = () => {
<Descriptions.Item label="场地面积">
{venueInfo.venueArea != null ? `${venueInfo.venueArea}` : '--'}
</Descriptions.Item>
<Descriptions.Item label="展位数量">
{venueInfo.floorCount ?? '--'}
</Descriptions.Item>
<Descriptions.Item label="展位数量">{venueInfo.floorCount ?? '--'}</Descriptions.Item>
<Descriptions.Item label="场地地址" span={2}>
{venueInfo.venueAddress || '--'}
</Descriptions.Item>
<Descriptions.Item label="联系电话">
{venueInfo.contactPhone || '--'}
</Descriptions.Item>
<Descriptions.Item label="联系电话">{venueInfo.contactPhone || '--'}</Descriptions.Item>
<Descriptions.Item label="乘坐线路" span={3}>
{venueInfo.routeLineNames || '--'}
</Descriptions.Item>
<Descriptions.Item label="经办日期">
{venueInfo.handleDate || '--'}
</Descriptions.Item>
<Descriptions.Item label="经办机构">
{venueInfo.handleOrg || '--'}
</Descriptions.Item>
<Descriptions.Item label="经办人">
{venueInfo.handler || '--'}
<Descriptions.Item label="经办日期">{venueInfo.handleDate || '--'}</Descriptions.Item>
<Descriptions.Item label="经办机构">{venueInfo.handleOrg || '--'}</Descriptions.Item>
<Descriptions.Item label="所属机构" span={3}>
{venueInfo.institutionNames || '--'}
</Descriptions.Item>
<Descriptions.Item label="经办人">{venueInfo.handler || '--'}</Descriptions.Item>
<Descriptions.Item label="备注" span={3}>
{venueInfo.remark || '--'}
</Descriptions.Item>
@@ -155,6 +173,9 @@ const VenueInfoDetail: React.FC = () => {
values={venueInfo}
venueTypeOptions={venueTypeOptions}
lineOptions={lineOptions}
institutionOptions={institutionOptions}
canCreateInstitution={access.hasPerms('cms:jobFairInstitution:add')}
onCreateInstitution={handleCreateInstitution}
onCreateDictOption={handleCreateDictOption}
onCancel={() => setEditModalOpen(false)}
onSubmit={async (values: VenueInfoForm) => {

View File

@@ -10,6 +10,10 @@ import {
} from '@ant-design/pro-components';
import { Button, Divider, Form, Input, Modal, Space } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import type {
JobFairInstitutionForm,
JobFairInstitutionItem,
} from '@/services/jobportal/jobFairInstitution';
import type {
VenueInfoDictForm,
VenueInfoForm,
@@ -21,8 +25,13 @@ interface EditModalProps {
values?: VenueInfoItem;
venueTypeOptions: any[];
lineOptions: any[];
institutionOptions: JobFairInstitutionItem[];
canCreateInstitution: boolean;
defaultHandler?: string;
onCreateDictOption: (values: VenueInfoDictForm) => Promise<boolean>;
onCreateInstitution: (
values: JobFairInstitutionForm,
) => Promise<JobFairInstitutionItem | undefined>;
onCancel: () => void;
onSubmit: (values: VenueInfoForm) => Promise<void>;
}
@@ -44,8 +53,11 @@ const EditModal: React.FC<EditModalProps> = ({
values,
venueTypeOptions,
lineOptions,
institutionOptions,
canCreateInstitution,
defaultHandler,
onCreateDictOption,
onCreateInstitution,
onCancel,
onSubmit,
}) => {
@@ -53,6 +65,9 @@ const EditModal: React.FC<EditModalProps> = ({
const [dictModalOpen, setDictModalOpen] = useState(false);
const [dictInputValue, setDictInputValue] = useState('');
const [dictSubmitLoading, setDictSubmitLoading] = useState(false);
const [institutionModalOpen, setInstitutionModalOpen] = useState(false);
const [institutionSubmitLoading, setInstitutionSubmitLoading] = useState(false);
const [institutionForm] = Form.useForm<JobFairInstitutionForm>();
useEffect(() => {
if (open) {
@@ -61,6 +76,7 @@ const EditModal: React.FC<EditModalProps> = ({
form.setFieldsValue({
...values,
routeLineIds: splitRouteLineIds(values.routeLineIds),
institutionIds: values.institutionIds || [],
});
} else {
form.setFieldsValue({
@@ -111,6 +127,45 @@ const EditModal: React.FC<EditModalProps> = ({
}
};
const renderInstitutionDropdown = (menu: React.ReactElement): React.ReactElement => (
<>
{menu}
{canCreateInstitution && (
<>
<Divider style={{ margin: '8px 0' }} />
<Space style={{ padding: '0 8px 8px' }}>
<Button
type="link"
icon={<PlusOutlined />}
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
institutionForm.resetFields();
setInstitutionModalOpen(true);
}}
>
</Button>
</Space>
</>
)}
</>
);
const handleInstitutionModalOk = async () => {
try {
const values = await institutionForm.validateFields();
setInstitutionSubmitLoading(true);
const created = await onCreateInstitution(values);
if (created) {
const selectedIds = (form.getFieldValue('institutionIds') || []) as number[];
form.setFieldValue('institutionIds', [...selectedIds, created.id]);
setInstitutionModalOpen(false);
}
} finally {
setInstitutionSubmitLoading(false);
}
};
return (
<ModalForm
title={values ? '编辑场地' : '添加场地'}
@@ -190,6 +245,21 @@ const EditModal: React.FC<EditModalProps> = ({
}}
placeholder="请选择乘坐线路"
/>
<ProFormSelect
name="institutionIds"
label="所属机构"
mode="multiple"
options={institutionOptions.map((item) => ({
label: item.institutionName,
value: item.id,
}))}
fieldProps={{
showSearch: true,
optionFilterProp: 'label',
dropdownRender: renderInstitutionDropdown,
}}
placeholder="请选择所属机构,可多选"
/>
<ProFormTextArea
name="remark"
label="备注"
@@ -224,6 +294,27 @@ const EditModal: React.FC<EditModalProps> = ({
onPressEnter={handleDictModalOk}
/>
</Modal>
<Modal
title="快速新增机构"
open={institutionModalOpen}
confirmLoading={institutionSubmitLoading}
onOk={handleInstitutionModalOk}
onCancel={() => setInstitutionModalOpen(false)}
destroyOnClose
>
<Form form={institutionForm} layout="vertical">
<Form.Item
name="institutionName"
label="机构名称"
rules={[{ required: true, message: '请输入机构名称' }]}
>
<Input maxLength={200} showCount placeholder="请输入机构名称" />
</Form.Item>
<Form.Item name="institutionIntro" label="机构简介">
<Input.TextArea maxLength={2000} showCount rows={4} placeholder="请输入机构简介" />
</Form.Item>
</Form>
</Modal>
</ModalForm>
);
};

View File

@@ -18,6 +18,14 @@ import type {
} from '@/services/jobportal/venueInfo';
import { getCmsLineList } from '@/services/area/subway';
import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict';
import {
addJobFairInstitution,
getJobFairInstitutionList,
} from '@/services/jobportal/jobFairInstitution';
import type {
JobFairInstitutionForm,
JobFairInstitutionItem,
} from '@/services/jobportal/jobFairInstitution';
import EditModal from './components/EditModal';
import VenueBoothMapModal from './components/VenueBoothMapModal';
@@ -33,6 +41,7 @@ const VenueInfoList: React.FC = () => {
const [venueTypeValueEnum, setVenueTypeValueEnum] = useState<Record<string, any>>({});
const [venueTypeOptions, setVenueTypeOptions] = useState<any[]>([]);
const [lineOptions, setLineOptions] = useState<any[]>([]);
const [institutionOptions, setInstitutionOptions] = useState<JobFairInstitutionItem[]>([]);
const defaultHandler =
initialState?.currentUser?.nickName || initialState?.currentUser?.userName || undefined;
@@ -58,10 +67,29 @@ const VenueInfoList: React.FC = () => {
}
}, []);
const refreshInstitutionOptions = useCallback(async () => {
const res = await getJobFairInstitutionList({ current: 1, pageSize: 9999 });
if (res.code === 200) {
setInstitutionOptions(res.rows || []);
}
}, []);
useEffect(() => {
refreshVenueTypeOptions();
refreshLineOptions();
}, [refreshVenueTypeOptions, refreshLineOptions]);
refreshInstitutionOptions();
}, [refreshVenueTypeOptions, refreshLineOptions, refreshInstitutionOptions]);
const handleCreateInstitution = async (values: JobFairInstitutionForm) => {
const res = await addJobFairInstitution(values);
if (res.code === 200 && res.data) {
await refreshInstitutionOptions();
message.success('机构新增成功');
return res.data;
}
message.error(res.msg || '机构新增失败');
return undefined;
};
const handleCreateDictOption = async (values: VenueInfoDictForm) => {
const res = await addVenueInfoDictOption(values);
@@ -118,6 +146,12 @@ const VenueInfoList: React.FC = () => {
dataIndex: 'venueAddress',
ellipsis: true,
},
{
title: '所属机构',
dataIndex: 'institutionNames',
ellipsis: true,
hideInSearch: true,
},
{
title: '联系电话',
dataIndex: 'contactPhone',
@@ -234,6 +268,9 @@ const VenueInfoList: React.FC = () => {
values={currentRow}
venueTypeOptions={venueTypeOptions}
lineOptions={lineOptions}
institutionOptions={institutionOptions}
canCreateInstitution={access.hasPerms('cms:jobFairInstitution:add')}
onCreateInstitution={handleCreateInstitution}
defaultHandler={defaultHandler}
onCreateDictOption={handleCreateDictOption}
onCancel={() => {

View File

@@ -0,0 +1,72 @@
import { request } from '@umijs/max';
export interface JobFairInstitutionItem {
id: number;
/** 机构名称 */
institutionName: string;
/** 机构简介 */
institutionIntro?: string;
createTime?: string;
updateTime?: string;
}
export interface JobFairInstitutionForm {
id?: number;
institutionName: string;
institutionIntro?: string;
}
export interface JobFairInstitutionListParams {
institutionName?: string;
current?: number;
pageSize?: number;
}
export interface JobFairInstitutionListResult {
code: number;
msg?: string;
total: number;
rows: JobFairInstitutionItem[];
}
export interface JobFairInstitutionResult {
code: number;
msg?: string;
data?: JobFairInstitutionItem;
}
const BASE_URL = '/api/cms/job-fair-institution';
export async function getJobFairInstitutionList(params?: JobFairInstitutionListParams) {
return request<JobFairInstitutionListResult>(`${BASE_URL}/list`, {
method: 'GET',
params,
});
}
export async function getJobFairInstitution(id: number) {
return request<JobFairInstitutionResult>(`${BASE_URL}/${id}`, {
method: 'GET',
});
}
export async function addJobFairInstitution(data: JobFairInstitutionForm) {
return request<JobFairInstitutionResult>(BASE_URL, {
method: 'POST',
data,
});
}
export async function updateJobFairInstitution(data: JobFairInstitutionForm) {
return request<JobFairInstitutionResult>(BASE_URL, {
method: 'PUT',
data,
});
}
export async function deleteJobFairInstitution(ids: number | number[]) {
const idPath = Array.isArray(ids) ? ids.join(',') : ids;
return request<JobFairInstitutionResult>(`${BASE_URL}/${idPath}`, {
method: 'DELETE',
});
}

View File

@@ -27,6 +27,10 @@ export interface VenueInfoItem {
handler?: string;
/** 备注 */
remark?: string;
/** 所属机构ID支持多个 */
institutionIds?: number[];
/** 所属机构名称,多个用顿号分隔 */
institutionNames?: string;
createTime?: string;
updateTime?: string;
}
@@ -63,6 +67,7 @@ export interface VenueInfoForm {
handleOrg?: string;
handler?: string;
remark?: string;
institutionIds?: number[];
}
/** 通用响应 */
@@ -103,6 +108,7 @@ const formatDate = (value: any) => {
const toApiPayload = (data: VenueInfoForm) => ({
...data,
routeLineIds: Array.isArray(data.routeLineIds) ? data.routeLineIds.join(',') : data.routeLineIds,
institutionIds: Array.isArray(data.institutionIds) ? data.institutionIds : [],
handleDate: formatDate(data.handleDate),
});