From 9e0afffef2c8cbe92a5ba659da148874e5a02233 Mon Sep 17 00:00:00 2001 From: lapuda <577732344@qq.com> Date: Thu, 25 Jun 2026 12:47:53 +0800 Subject: [PATCH] feat: add venue information management and detail view - Added routes for venue information maintenance and detail views. - Implemented venue information detail page with data fetching and display. - Created edit modal for adding and editing venue information with form validation. - Integrated venue type and line options for selection in the edit modal. - Enhanced outdoor fair management to include venue binding and details. - Updated services to handle venue information CRUD operations. --- config/routes.ts | 10 + .../Detail/components/FairInfoTab.tsx | 192 +++++++++++--- .../Outdoorfair/components/EditModal.tsx | 36 ++- src/pages/Jobfair/Outdoorfair/index.tsx | 40 +++ src/pages/Jobfair/Venueinfo/Detail/index.tsx | 83 ++++++ .../Venueinfo/components/EditModal.tsx | 231 ++++++++++++++++ src/pages/Jobfair/Venueinfo/index.tsx | 247 ++++++++++++++++++ src/services/jobportal/outdoorFair.ts | 7 + src/services/jobportal/venueInfo.ts | 158 +++++++++++ 9 files changed, 958 insertions(+), 46 deletions(-) create mode 100644 src/pages/Jobfair/Venueinfo/Detail/index.tsx create mode 100644 src/pages/Jobfair/Venueinfo/components/EditModal.tsx create mode 100644 src/pages/Jobfair/Venueinfo/index.tsx create mode 100644 src/services/jobportal/venueInfo.ts diff --git a/config/routes.ts b/config/routes.ts index 7ee0c81..68035a2 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -226,6 +226,16 @@ export default [ path: '/jobfair/outdoor-fair', component: './Jobfair/Outdoorfair', }, + { + name: '场地信息维护', + path: '/jobfair/venue-info', + component: './Jobfair/Venueinfo', + }, + { + name: '场地信息详情', + path: '/jobfair/venue-info/detail', + component: './Jobfair/Venueinfo/Detail', + }, { name: '户外招聘会详情', path: '/jobfair/outdoor-fair/detail', diff --git a/src/pages/Jobfair/Outdoorfair/Detail/components/FairInfoTab.tsx b/src/pages/Jobfair/Outdoorfair/Detail/components/FairInfoTab.tsx index be2c183..ead4c35 100644 --- a/src/pages/Jobfair/Outdoorfair/Detail/components/FairInfoTab.tsx +++ b/src/pages/Jobfair/Outdoorfair/Detail/components/FairInfoTab.tsx @@ -1,10 +1,12 @@ -import React, { useRef, useState } from 'react'; -import { useAccess } from '@umijs/max'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { history, useAccess } from '@umijs/max'; import { Button, Card, Descriptions, List, Tag, message, Modal, Space } from 'antd'; import { ActionType, ProTable } from '@ant-design/pro-components'; import { PlusOutlined, DeleteOutlined, FormOutlined } from '@ant-design/icons'; -import type { OutdoorFairItem } from '@/services/jobportal/outdoorFair'; -import { updateOutdoorFair } from '@/services/jobportal/outdoorFair'; +import type { OutdoorFairDictForm, OutdoorFairItem } from '@/services/jobportal/outdoorFair'; +import { addOutdoorFairDictOption, updateOutdoorFair } from '@/services/jobportal/outdoorFair'; +import { getVenueInfoList } from '@/services/jobportal/venueInfo'; +import { getDictSelectOption } from '@/services/system/dict'; import { getParticipatingCompanies, addParticipatingCompany, @@ -17,6 +19,9 @@ import EditModal from '@/pages/Jobfair/Outdoorfair/components/EditModal'; import CompanyAddModal from './CompanyAddModal'; import JobEditModal from './JobEditModal'; +const OUTDOOR_FAIR_TYPE_DICT = 'outdoor_fair_type'; +const OUTDOOR_FAIR_REGION_DICT = 'outdoor_fair_region'; + interface Props { fairId: number; fairInfo: OutdoorFairItem; @@ -31,11 +36,53 @@ const FairInfoTab: React.FC = ({ fairId, fairInfo, onFairUpdate }) => { const [jobModalOpen, setJobModalOpen] = useState(false); const [selectedCompany, setSelectedCompany] = useState(null); const [editingJob, setEditingJob] = useState(null); + const [fairTypeOptions, setFairTypeOptions] = useState([]); + const [regionOptions, setRegionOptions] = useState([]); + const [venueOptions, setVenueOptions] = useState([]); + + const refreshEditModalOptions = useCallback(async () => { + const [typeOptions, regionSelectOptions, venueRes] = await Promise.all([ + getDictSelectOption(OUTDOOR_FAIR_TYPE_DICT), + getDictSelectOption(OUTDOOR_FAIR_REGION_DICT), + getVenueInfoList({ current: 1, pageSize: 999 }), + ]); + setFairTypeOptions(typeOptions); + setRegionOptions(regionSelectOptions); + if (venueRes.code === 200) { + setVenueOptions( + (venueRes.rows || []).map((item) => ({ + label: item.venueName, + value: item.id, + venueAddress: item.venueAddress, + floorCount: item.floorCount, + })), + ); + } + }, []); + + useEffect(() => { + refreshEditModalOptions(); + }, [refreshEditModalOptions]); + + const handleCreateDictOption = async (values: OutdoorFairDictForm) => { + const res = await addOutdoorFairDictOption(values); + if (res.code === 200) { + await refreshEditModalOptions(); + message.success('字典项新增成功'); + return true; + } + message.error(res.msg || '字典项新增失败'); + return false; + }; const fairTypeColors: Record = { - '综合类': 'blue', '校园招聘': 'green', '行业专场': 'orange', - '高新技术专场': 'purple', '智能制造专场': 'cyan', '文旅专场': 'magenta', - '其他': 'default', + 综合类: 'blue', + 校园招聘: 'green', + 行业专场: 'orange', + 高新技术专场: 'purple', + 智能制造专场: 'cyan', + 文旅专场: 'magenta', + 其他: 'default', }; return ( @@ -57,19 +104,38 @@ const FairInfoTab: React.FC = ({ fairId, fairInfo, onFairUpdate }) => { style={{ marginBottom: 16 }} > - {fairInfo.title} + + {fairInfo.title} + {fairInfo.hostUnit} {fairInfo.fairType} {fairInfo.region} - {fairInfo.address} + + {fairInfo.venueId && fairInfo.venueName ? ( + + ) : ( + '--' + )} + + + {fairInfo.address} + {fairInfo.boothCount} {fairInfo.holdTime} {fairInfo.endTime} {fairInfo.applyStartTime} {fairInfo.applyEndTime} - {fairInfo.onlineApply ? '是' : '否'} + + {fairInfo.onlineApply ? '是' : '否'} + @@ -77,11 +143,7 @@ const FairInfoTab: React.FC = ({ fairId, fairInfo, onFairUpdate }) => { } - onClick={() => setCompanyModalOpen(true)} - > + } @@ -112,20 +174,32 @@ const FairInfoTab: React.FC = ({ fairId, fairInfo, onFairUpdate }) => { { setEditingJob(job); setSelectedCompany(record); setJobModalOpen(true); }} + key="edit" + type="link" + size="small" + onClick={() => { + setEditingJob(job); + setSelectedCompany(record); + setJobModalOpen(true); + }} > 编辑 , , - + ) : ( + '--' + ), + }, { title: '举办地址', dataIndex: 'address', @@ -216,6 +254,7 @@ const OutdoorFairList: React.FC = () => { hostUnit: params.hostUnit, fairType: params.fairType, region: params.region, + venueId: params.venueId, } as OutdoorFairListParams); return { data: res.rows, total: res.total, success: true }; }} @@ -239,6 +278,7 @@ const OutdoorFairList: React.FC = () => { values={currentRow} fairTypeOptions={fairTypeOptions} regionOptions={regionOptions} + venueOptions={venueOptions} onCreateDictOption={handleCreateDictOption} onCancel={() => { setModalVisible(false); diff --git a/src/pages/Jobfair/Venueinfo/Detail/index.tsx b/src/pages/Jobfair/Venueinfo/Detail/index.tsx new file mode 100644 index 0000000..a81c75e --- /dev/null +++ b/src/pages/Jobfair/Venueinfo/Detail/index.tsx @@ -0,0 +1,83 @@ +import React, { useEffect, useState } from 'react'; +import { history, useSearchParams } from '@umijs/max'; +import { PageContainer } from '@ant-design/pro-components'; +import { Button, Card, Descriptions, Spin, Tag, message } from 'antd'; +import { ArrowLeftOutlined } from '@ant-design/icons'; +import { getVenueInfo } from '@/services/jobportal/venueInfo'; +import type { VenueInfoItem } from '@/services/jobportal/venueInfo'; + +const VenueInfoDetail: React.FC = () => { + const [searchParams] = useSearchParams(); + const venueId = Number(searchParams.get('id')); + const [venueInfo, setVenueInfo] = useState(); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchVenueInfo = async () => { + if (!venueId) { + setLoading(false); + return; + } + setLoading(true); + try { + const res = await getVenueInfo(venueId); + if (res.code === 200 && res.data) { + setVenueInfo(res.data); + } else { + message.error(res.msg || '未找到该场地信息'); + } + } finally { + setLoading(false); + } + }; + fetchVenueInfo(); + }, [venueId]); + + return ( + history.back(), + extra: [ + , + ], + }} + > + + {venueInfo && ( + + + + {venueInfo.venueType} + + + {venueInfo.venueName} + + {venueInfo.venueArea ?? '--'} + {venueInfo.floorCount ?? '--'} + + {venueInfo.contactPhone || '--'} + + + {venueInfo.venueAddress} + + + {venueInfo.routeLineNames || '--'} + + {venueInfo.handleDate || '--'} + {venueInfo.handleOrg || '--'} + {venueInfo.handler || '--'} + + {venueInfo.remark || '--'} + + + + )} + + + ); +}; + +export default VenueInfoDetail; diff --git a/src/pages/Jobfair/Venueinfo/components/EditModal.tsx b/src/pages/Jobfair/Venueinfo/components/EditModal.tsx new file mode 100644 index 0000000..2845309 --- /dev/null +++ b/src/pages/Jobfair/Venueinfo/components/EditModal.tsx @@ -0,0 +1,231 @@ +import React, { useEffect, useState } from 'react'; +import { + ModalForm, + ProForm, + ProFormDatePicker, + ProFormDigit, + ProFormSelect, + ProFormText, + ProFormTextArea, +} from '@ant-design/pro-components'; +import { Button, Divider, Form, Input, Modal, Space } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; +import type { + VenueInfoDictForm, + VenueInfoForm, + VenueInfoItem, +} from '@/services/jobportal/venueInfo'; + +interface EditModalProps { + open: boolean; + values?: VenueInfoItem; + venueTypeOptions: any[]; + lineOptions: any[]; + defaultHandler?: string; + onCreateDictOption: (values: VenueInfoDictForm) => Promise; + onCancel: () => void; + onSubmit: (values: VenueInfoForm) => Promise; +} + +const VENUE_TYPE_DICT = 'venue_info_type'; + +const splitRouteLineIds = (routeLineIds?: string) => { + if (!routeLineIds) { + return []; + } + return routeLineIds + .split(',') + .map((item) => item.trim()) + .filter(Boolean); +}; + +const EditModal: React.FC = ({ + open, + values, + venueTypeOptions, + lineOptions, + defaultHandler, + onCreateDictOption, + onCancel, + onSubmit, +}) => { + const [form] = Form.useForm(); + const [dictModalOpen, setDictModalOpen] = useState(false); + const [dictInputValue, setDictInputValue] = useState(''); + const [dictSubmitLoading, setDictSubmitLoading] = useState(false); + + useEffect(() => { + if (open) { + form.resetFields(); + if (values) { + form.setFieldsValue({ + ...values, + routeLineIds: splitRouteLineIds(values.routeLineIds), + }); + } else { + form.setFieldsValue({ + handleDate: new Date(), + handler: defaultHandler, + }); + } + } + }, [open, values, defaultHandler, form]); + + const renderVenueTypeDropdown = (menu: React.ReactElement): React.ReactElement => ( + <> + {menu} + + + + + + ); + + const handleDictModalOk = async () => { + const dictLabel = dictInputValue.trim(); + if (!dictLabel) { + return; + } + setDictSubmitLoading(true); + try { + const success = await onCreateDictOption({ + dictType: VENUE_TYPE_DICT, + dictLabel, + }); + if (success) { + form.setFieldValue('venueType', dictLabel); + setDictModalOpen(false); + } + } finally { + setDictSubmitLoading(false); + } + }; + + return ( + { + const selectedLineIds = (formValues.routeLineIds || []) as string[]; + const selectedLineNames = lineOptions + .filter((option) => selectedLineIds.includes(String(option.value))) + .map((option) => option.label) + .join(','); + await onSubmit({ + ...formValues, + id: values?.id, + routeLineIds: selectedLineIds, + routeLineNames: selectedLineNames, + } as VenueInfoForm); + }} + > + + ); +}; + +export default EditModal; diff --git a/src/pages/Jobfair/Venueinfo/index.tsx b/src/pages/Jobfair/Venueinfo/index.tsx new file mode 100644 index 0000000..a116601 --- /dev/null +++ b/src/pages/Jobfair/Venueinfo/index.tsx @@ -0,0 +1,247 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useAccess, useModel } from '@umijs/max'; +import { Button, message, Modal } from 'antd'; +import { ActionType, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components'; +import { DeleteOutlined, FormOutlined, PlusOutlined } from '@ant-design/icons'; +import { + addVenueInfo, + addVenueInfoDictOption, + deleteVenueInfo, + getVenueInfoList, + updateVenueInfo, +} from '@/services/jobportal/venueInfo'; +import type { + VenueInfoDictForm, + VenueInfoForm, + VenueInfoItem, + VenueInfoListParams, +} from '@/services/jobportal/venueInfo'; +import { getCmsLineList } from '@/services/area/subway'; +import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict'; +import EditModal from './components/EditModal'; + +const VENUE_TYPE_DICT = 'venue_info_type'; + +const VenueInfoList: React.FC = () => { + const access = useAccess(); + const { initialState } = useModel('@@initialState'); + const actionRef = useRef(); + const [modalVisible, setModalVisible] = useState(false); + const [currentRow, setCurrentRow] = useState(); + const [venueTypeValueEnum, setVenueTypeValueEnum] = useState>({}); + const [venueTypeOptions, setVenueTypeOptions] = useState([]); + const [lineOptions, setLineOptions] = useState([]); + + const defaultHandler = + initialState?.currentUser?.nickName || initialState?.currentUser?.userName || undefined; + + const refreshVenueTypeOptions = useCallback(async () => { + const [valueEnum, options] = await Promise.all([ + getDictValueEnum(VENUE_TYPE_DICT), + getDictSelectOption(VENUE_TYPE_DICT), + ]); + setVenueTypeValueEnum(valueEnum); + setVenueTypeOptions(options); + }, []); + + const refreshLineOptions = useCallback(async () => { + const res = await getCmsLineList({ current: '1', pageSize: '999' } as any); + if (res.code === 200) { + setLineOptions( + (res.rows || []).map((item) => ({ + label: item.lineName, + value: String(item.lineId), + })), + ); + } + }, []); + + useEffect(() => { + refreshVenueTypeOptions(); + refreshLineOptions(); + }, [refreshVenueTypeOptions, refreshLineOptions]); + + const handleCreateDictOption = async (values: VenueInfoDictForm) => { + const res = await addVenueInfoDictOption(values); + if (res.code === 200) { + await refreshVenueTypeOptions(); + message.success('场地类型新增成功'); + return true; + } + message.error(res.msg || '场地类型新增失败'); + return false; + }; + + const handleDelete = async (id: number) => { + Modal.confirm({ + title: '确认删除', + content: '确定要删除该场地信息吗?', + onOk: async () => { + const res = await deleteVenueInfo(id); + if (res.code === 200) { + message.success('删除成功'); + actionRef.current?.reload(); + } else { + message.error(res.msg || '删除失败'); + } + }, + }); + }; + + const columns: ProColumns[] = [ + { + title: '场地类型', + dataIndex: 'venueType', + ellipsis: true, + valueType: 'select', + valueEnum: venueTypeValueEnum, + }, + { + title: '场地名称', + dataIndex: 'venueName', + ellipsis: true, + }, + { + title: '场地面积', + dataIndex: 'venueArea', + hideInSearch: true, + }, + { + title: '展位数量', + dataIndex: 'floorCount', + hideInSearch: true, + }, + { + title: '场地地址', + dataIndex: 'venueAddress', + ellipsis: true, + }, + { + title: '联系电话', + dataIndex: 'contactPhone', + hideInSearch: true, + }, + { + title: '乘坐线路', + dataIndex: 'routeLineNames', + ellipsis: true, + hideInSearch: true, + }, + { + title: '经办日期', + dataIndex: 'handleDate', + valueType: 'date', + hideInSearch: true, + }, + { + title: '经办机构', + dataIndex: 'handleOrg', + ellipsis: true, + hideInSearch: true, + }, + { + title: '经办人', + dataIndex: 'handler', + hideInSearch: true, + }, + { + title: '备注', + dataIndex: 'remark', + ellipsis: true, + hideInSearch: true, + }, + { + title: '操作', + valueType: 'option', + width: 140, + fixed: 'right', + render: (_, record) => [ + , + , + ], + }, + ]; + + return ( + + + headerTitle="场地信息维护列表" + actionRef={actionRef} + rowKey="id" + columns={columns} + scroll={{ x: 'max-content' }} + request={async (params) => { + const res = await getVenueInfoList({ + current: params.current, + pageSize: params.pageSize, + venueType: params.venueType, + venueName: params.venueName, + venueAddress: params.venueAddress, + } as VenueInfoListParams); + return { data: res.rows, total: res.total, success: true }; + }} + toolBarRender={() => [ + , + ]} + /> + { + setModalVisible(false); + setCurrentRow(undefined); + }} + onSubmit={async (values: VenueInfoForm) => { + const res = values.id ? await updateVenueInfo(values) : await addVenueInfo(values); + if (res.code === 200) { + message.success(values.id ? '修改成功' : '新增成功'); + setModalVisible(false); + setCurrentRow(undefined); + actionRef.current?.reload(); + } else { + message.error(res.msg || '操作失败'); + } + }} + /> + + ); +}; + +export default VenueInfoList; diff --git a/src/services/jobportal/outdoorFair.ts b/src/services/jobportal/outdoorFair.ts index 336b6d7..76cdf21 100644 --- a/src/services/jobportal/outdoorFair.ts +++ b/src/services/jobportal/outdoorFair.ts @@ -11,6 +11,10 @@ export interface OutdoorFairItem { fairType: string; /** 举办区域 */ region: string; + /** 绑定场地ID */ + venueId?: number; + /** 绑定场地 */ + venueName?: string; /** 举办地址 */ address: string; /** 展位数量 */ @@ -37,6 +41,7 @@ export interface OutdoorFairListParams { hostUnit?: string; fairType?: string; region?: string; + venueId?: number; current?: number; pageSize?: number; } @@ -56,6 +61,8 @@ export interface OutdoorFairForm { hostUnit: string; fairType: string; region: string; + venueId?: number; + venueName?: string; address: string; boothCount: number; holdTime: string; diff --git a/src/services/jobportal/venueInfo.ts b/src/services/jobportal/venueInfo.ts new file mode 100644 index 0000000..0ed9348 --- /dev/null +++ b/src/services/jobportal/venueInfo.ts @@ -0,0 +1,158 @@ +import { request } from '@umijs/max'; + +/** 场地信息列表项 */ +export interface VenueInfoItem { + id: number; + /** 场地类型 */ + venueType: string; + /** 场地名称 */ + venueName: string; + /** 场地面积 */ + venueArea?: number; + /** 展位数量 */ + floorCount?: number; + /** 场地地址 */ + venueAddress: string; + /** 联系电话 */ + contactPhone?: string; + /** 乘坐线路ID,英文逗号分隔 */ + routeLineIds?: string; + /** 乘坐线路名称,英文逗号分隔 */ + routeLineNames?: string; + /** 经办日期 */ + handleDate?: string; + /** 经办机构 */ + handleOrg?: string; + /** 经办人 */ + handler?: string; + /** 备注 */ + remark?: string; + createTime?: string; + updateTime?: string; +} + +/** 场地信息查询参数 */ +export interface VenueInfoListParams { + venueType?: string; + venueName?: string; + venueAddress?: string; + current?: number; + pageSize?: number; +} + +/** 场地信息列表响应 */ +export interface VenueInfoListResult { + code: number; + msg?: string; + total: number; + rows: VenueInfoItem[]; +} + +/** 场地信息新增/编辑表单 */ +export interface VenueInfoForm { + id?: number; + venueType: string; + venueName: string; + venueArea?: number; + floorCount?: number; + venueAddress: string; + contactPhone?: string; + routeLineIds?: string | string[]; + routeLineNames?: string; + handleDate?: string; + handleOrg?: string; + handler?: string; + remark?: string; +} + +/** 通用响应 */ +export interface VenueInfoCommonResult { + code: number; + msg?: string; + data?: VenueInfoItem; +} + +/** 场地类型字典新增表单 */ +export interface VenueInfoDictForm { + dictType: 'venue_info_type'; + dictLabel: string; +} + +const CMS_VENUE_INFO_BASE = '/api/cms/venue-info'; + +const formatDate = (value: any) => { + if (value && typeof value.format === 'function') { + return value.format('YYYY-MM-DD'); + } + return value; +}; + +const toApiPayload = (data: VenueInfoForm) => ({ + ...data, + routeLineIds: Array.isArray(data.routeLineIds) ? data.routeLineIds.join(',') : data.routeLineIds, + handleDate: formatDate(data.handleDate), +}); + +/** + * 获取场地信息列表 + * GET /api/cms/venue-info/list + */ +export async function getVenueInfoList(params?: VenueInfoListParams) { + return request(`${CMS_VENUE_INFO_BASE}/list`, { + method: 'GET', + params, + }); +} + +/** + * 获取场地信息详情 + * GET /api/cms/venue-info/{id} + */ +export async function getVenueInfo(id: number) { + return request(`${CMS_VENUE_INFO_BASE}/${id}`, { + method: 'GET', + }); +} + +/** + * 新增场地信息 + * POST /api/cms/venue-info + */ +export async function addVenueInfo(data: VenueInfoForm) { + return request(CMS_VENUE_INFO_BASE, { + method: 'POST', + data: toApiPayload(data), + }); +} + +/** + * 修改场地信息 + * PUT /api/cms/venue-info + */ +export async function updateVenueInfo(data: VenueInfoForm) { + return request(CMS_VENUE_INFO_BASE, { + method: 'PUT', + data: toApiPayload(data), + }); +} + +/** + * 删除场地信息 + * DELETE /api/cms/venue-info/{id} + */ +export async function deleteVenueInfo(id: number) { + return request(`${CMS_VENUE_INFO_BASE}/${id}`, { + method: 'DELETE', + }); +} + +/** + * 新增场地信息维护页面使用的字典项 + * POST /api/cms/venue-info/dict-data + */ +export async function addVenueInfoDictOption(data: VenueInfoDictForm) { + return request(`${CMS_VENUE_INFO_BASE}/dict-data`, { + method: 'POST', + data, + }); +}