Refactor outdoor fair management:

- Remove .DS_Store file.
- Update .gitignore to include .DS_Store.
- Modify proxy configuration to use environment variable for backend URL.
- Correct component paths in routes.
- Implement outdoor fair detail fetching with improved API call.
- Enhance EditModal for outdoor fair with dynamic dictionary options.
- Add functionality to upload outdoor fair photos.
- Update service layer to handle outdoor fair dictionary options.
This commit is contained in:
2026-06-25 11:30:40 +08:00
parent 5ecce431db
commit 93f39886d9
9 changed files with 555 additions and 245 deletions

View File

@@ -3,7 +3,7 @@ import { history, useSearchParams } from '@umijs/max';
import { PageContainer } from '@ant-design/pro-components';
import { Tabs, Button, Spin, message } from 'antd';
import { ArrowLeftOutlined } from '@ant-design/icons';
import { getOutdoorFairList } from '@/services/jobportal/outdoorFair';
import { getOutdoorFairInfo } from '@/services/jobportal/outdoorFair';
import type { OutdoorFairItem } from '@/services/jobportal/outdoorFair';
import FairInfoTab from './components/FairInfoTab';
import BoothTab from './components/BoothTab';
@@ -22,12 +22,11 @@ const OutdoorFairDetail: React.FC = () => {
const fetchFairInfo = async () => {
setLoading(true);
try {
const res = await getOutdoorFairList({ current: 1, pageSize: 100 });
const found = res.rows.find((item) => item.id === fairId);
if (found) {
setFairInfo(found);
const res = await getOutdoorFairInfo(fairId);
if (res.code === 200 && res.data) {
setFairInfo(res.data);
} else {
message.error('未找到该招聘会信息');
message.error(res.msg || '未找到该招聘会信息');
}
} finally {
setLoading(false);
@@ -72,7 +71,13 @@ const OutdoorFairDetail: React.FC = () => {
children: (() => {
switch (tab.key) {
case 'fair-info':
return <FairInfoTab fairId={fairId} fairInfo={fairInfo} onFairUpdate={fetchFairInfo} />;
return (
<FairInfoTab
fairId={fairId}
fairInfo={fairInfo}
onFairUpdate={fetchFairInfo}
/>
);
case 'booth':
return <BoothTab fairId={fairId} />;
case 'company-review':

View File

@@ -1,55 +1,54 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import {
ModalForm,
ProForm,
ProFormText,
ProFormDigit,
ProFormDateTimePicker,
ProFormSwitch,
ProFormSelect,
ProFormText,
} from '@ant-design/pro-components';
import { Form } from 'antd';
import type { OutdoorFairItem, OutdoorFairForm } from '@/services/jobportal/outdoorFair';
import { Button, Divider, Form, Image, Input, message, Modal, Space, Upload } from 'antd';
import { PlusOutlined, UploadOutlined } from '@ant-design/icons';
import type {
OutdoorFairItem,
OutdoorFairForm,
OutdoorFairDictForm,
} from '@/services/jobportal/outdoorFair';
import { uploadOutdoorFairPhoto } from '@/services/jobportal/outdoorFair';
interface EditModalProps {
open: boolean;
values?: OutdoorFairItem;
fairTypeOptions: any[];
regionOptions: any[];
onCreateDictOption: (values: OutdoorFairDictForm) => Promise<boolean>;
onCancel: () => void;
onSubmit: (values: OutdoorFairForm) => Promise<void>;
}
/** 招聘会类型选项 */
const FAIR_TYPE_OPTIONS = [
{ label: '综合类', value: '综合类' },
{ label: '校园招聘', value: '校园招聘' },
{ label: '行业专场', value: '行业专场' },
{ label: '高新技术专场', value: '高新技术专场' },
{ label: '智能制造专场', value: '智能制造专场' },
{ label: '文旅专场', value: '文旅专场' },
{ label: '其他', value: '其他' },
];
type DictAddTarget = {
dictType: OutdoorFairDictForm['dictType'];
fieldName: keyof Pick<OutdoorFairForm, 'fairType' | 'region'>;
title: string;
placeholder: string;
};
/** 举办区域选项 */
const REGION_OPTIONS = [
{ label: '石河子市', value: '石河子市' },
{ label: '第一师(阿拉尔市)', value: '第一师(阿拉尔市)' },
{ label: '第二师(铁门关市)', value: '第二师(铁门关市)' },
{ label: '第三师(图木舒克市)', value: '第三师(图木舒克市)' },
{ label: '第四师(可克达拉市)', value: '第四师(可克达拉市)' },
{ label: '第五师(双河市)', value: '第五师(双河市)' },
{ label: '第六师(五家渠市)', value: '第六师(五家渠市)' },
{ label: '第七师(胡杨河市)', value: '第七师(胡杨河市)' },
{ label: '第八师(石河子市)', value: '第八师(石河子市)' },
{ label: '第九师(白杨市)', value: '第九师(白杨市)' },
{ label: '第十师(北屯市)', value: '第十师(北屯市)' },
{ label: '第十一师(建工师)', value: '第十一师(建工师)' },
{ label: '第十二师', value: '第十二师' },
{ label: '第十三师(新星市)', value: '第十三师(新星市)' },
{ label: '第十四师(昆玉市)', value: '第十四师(昆玉市)' },
];
const EditModal: React.FC<EditModalProps> = ({ open, values, onCancel, onSubmit }) => {
const EditModal: React.FC<EditModalProps> = ({
open,
values,
fairTypeOptions,
regionOptions,
onCreateDictOption,
onCancel,
onSubmit,
}) => {
const [form] = Form.useForm();
const [dictModalOpen, setDictModalOpen] = useState(false);
const [dictInputValue, setDictInputValue] = useState('');
const [dictSubmitLoading, setDictSubmitLoading] = useState(false);
const [dictAddTarget, setDictAddTarget] = useState<DictAddTarget>();
const photoUrl = Form.useWatch('photoUrl', form);
useEffect(() => {
if (open) {
@@ -60,6 +59,85 @@ const EditModal: React.FC<EditModalProps> = ({ open, values, onCancel, onSubmit
}
}, [open, values, form]);
const openDictAddModal = (target: DictAddTarget) => {
setDictAddTarget(target);
setDictInputValue('');
setDictModalOpen(true);
};
const renderDropdownWithCreate = (
menu: React.ReactNode,
target: DictAddTarget,
): React.ReactNode => (
<>
{menu}
<Divider style={{ margin: '8px 0' }} />
<Space style={{ padding: '0 8px 8px' }}>
<Button
type="link"
icon={<PlusOutlined />}
onMouseDown={(event) => event.preventDefault()}
onClick={() => openDictAddModal(target)}
>
{target.title}
</Button>
</Space>
</>
);
const handleDictModalOk = async () => {
const dictLabel = dictInputValue.trim();
if (!dictAddTarget || !dictLabel) {
return;
}
setDictSubmitLoading(true);
try {
const success = await onCreateDictOption({
dictType: dictAddTarget.dictType,
dictLabel,
});
if (success) {
form.setFieldValue(dictAddTarget.fieldName, dictLabel);
setDictModalOpen(false);
}
} finally {
setDictSubmitLoading(false);
}
};
const beforePhotoUpload = (file: File) => {
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('只能上传图片文件');
return Upload.LIST_IGNORE;
}
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
message.error('图片大小不能超过 10MB');
return Upload.LIST_IGNORE;
}
return true;
};
const handlePhotoUpload = async (options: any) => {
const { file, onSuccess, onError } = options;
try {
const res = await uploadOutdoorFairPhoto(file as File);
if (res.code === 200 && res.url) {
form.setFieldValue('photoUrl', res.url);
message.success('照片上传成功');
onSuccess?.(res);
} else {
const error = new Error(res.msg || '照片上传失败');
message.error(error.message);
onError?.(error);
}
} catch (error: any) {
message.error(error?.message || '照片上传失败');
onError?.(error);
}
};
return (
<ModalForm
title={values ? '编辑户外招聘会' : '新增户外招聘会'}
@@ -72,6 +150,11 @@ const EditModal: React.FC<EditModalProps> = ({ open, values, onCancel, onSubmit
}}
>
<ProFormText name="id" hidden />
<ProFormText
name="photoUrl"
hidden
rules={[{ required: true, message: '请上传招聘会照片' }]}
/>
<ProFormText
name="title"
label="招聘会标题"
@@ -90,7 +173,17 @@ const EditModal: React.FC<EditModalProps> = ({ open, values, onCancel, onSubmit
name="fairType"
label="招聘会类型"
width="md"
options={FAIR_TYPE_OPTIONS}
options={fairTypeOptions}
fieldProps={{
showSearch: true,
dropdownRender: (menu) =>
renderDropdownWithCreate(menu, {
dictType: 'outdoor_fair_type',
fieldName: 'fairType',
title: '新增招聘会类型',
placeholder: '请输入招聘会类型',
}),
}}
rules={[{ required: true, message: '请选择招聘会类型' }]}
placeholder="请选择招聘会类型"
/>
@@ -100,7 +193,17 @@ const EditModal: React.FC<EditModalProps> = ({ open, values, onCancel, onSubmit
name="region"
label="举办区域"
width="md"
options={REGION_OPTIONS}
options={regionOptions}
fieldProps={{
showSearch: true,
dropdownRender: (menu) =>
renderDropdownWithCreate(menu, {
dictType: 'outdoor_fair_region',
fieldName: 'region',
title: '新增举办区域',
placeholder: '请输入举办区域',
}),
}}
rules={[{ required: true, message: '请选择举办区域' }]}
placeholder="请选择举办区域"
/>
@@ -157,12 +260,47 @@ const EditModal: React.FC<EditModalProps> = ({ open, values, onCancel, onSubmit
colProps={{ md: 24 }}
initialValue={false}
/>
<ProFormText
name="photoUrl"
label="招聘会照片链接"
placeholder="请输入照片URL地址"
rules={[{ required: true, message: '请输入招聘会照片链接' }]}
/>
<ProForm.Item label="招聘会照片" required>
<Space direction="vertical" size={12}>
<Upload
accept="image/*"
maxCount={1}
showUploadList={false}
beforeUpload={beforePhotoUpload}
customRequest={handlePhotoUpload}
>
<Button icon={<UploadOutlined />}></Button>
</Upload>
{photoUrl ? (
<Image
src={photoUrl}
width={180}
height={120}
style={{ objectFit: 'cover', borderRadius: 6 }}
/>
) : (
<span style={{ color: '#999' }}> jpgjpegpnggifbmp 10MB</span>
)}
</Space>
</ProForm.Item>
<Modal
title={dictAddTarget?.title}
open={dictModalOpen}
confirmLoading={dictSubmitLoading}
onOk={handleDictModalOk}
onCancel={() => setDictModalOpen(false)}
okButtonProps={{ disabled: !dictInputValue.trim() }}
destroyOnClose
>
<Input
value={dictInputValue}
maxLength={100}
showCount
placeholder={dictAddTarget?.placeholder}
onChange={(event) => setDictInputValue(event.target.value)}
onPressEnter={handleDictModalOk}
/>
</Modal>
</ModalForm>
);
};

View File

@@ -1,4 +1,4 @@
import React, { useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useAccess, history } from '@umijs/max';
import { Button, message, Modal } from 'antd';
import { ActionType, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
@@ -8,19 +8,57 @@ import {
addOutdoorFair,
updateOutdoorFair,
deleteOutdoorFair,
addOutdoorFairDictOption,
} from '@/services/jobportal/outdoorFair';
import type {
OutdoorFairItem,
OutdoorFairListParams,
OutdoorFairForm,
OutdoorFairDictForm,
} from '@/services/jobportal/outdoorFair';
import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict';
import EditModal from './components/EditModal';
const OUTDOOR_FAIR_TYPE_DICT = 'outdoor_fair_type';
const OUTDOOR_FAIR_REGION_DICT = 'outdoor_fair_region';
const OutdoorFairList: React.FC = () => {
const access = useAccess();
const actionRef = useRef<ActionType>();
const [modalVisible, setModalVisible] = useState(false);
const [currentRow, setCurrentRow] = useState<OutdoorFairItem>();
const [fairTypeValueEnum, setFairTypeValueEnum] = useState<Record<string, any>>({});
const [fairTypeOptions, setFairTypeOptions] = useState<any[]>([]);
const [regionValueEnum, setRegionValueEnum] = useState<Record<string, any>>({});
const [regionOptions, setRegionOptions] = useState<any[]>([]);
const refreshDictOptions = useCallback(async () => {
const [typeValueEnum, typeOptions, regionEnum, regionSelectOptions] = await Promise.all([
getDictValueEnum(OUTDOOR_FAIR_TYPE_DICT),
getDictSelectOption(OUTDOOR_FAIR_TYPE_DICT),
getDictValueEnum(OUTDOOR_FAIR_REGION_DICT),
getDictSelectOption(OUTDOOR_FAIR_REGION_DICT),
]);
setFairTypeValueEnum(typeValueEnum);
setFairTypeOptions(typeOptions);
setRegionValueEnum(regionEnum);
setRegionOptions(regionSelectOptions);
}, []);
useEffect(() => {
refreshDictOptions();
}, [refreshDictOptions]);
const handleCreateDictOption = async (values: OutdoorFairDictForm) => {
const res = await addOutdoorFairDictOption(values);
if (res.code === 200) {
await refreshDictOptions();
message.success('字典项新增成功');
return true;
}
message.error(res.msg || '字典项新增失败');
return false;
};
const handleDelete = async (id: number) => {
Modal.confirm({
@@ -53,13 +91,15 @@ const OutdoorFairList: React.FC = () => {
title: '招聘会类型',
dataIndex: 'fairType',
ellipsis: true,
hideInSearch: true,
valueType: 'select',
valueEnum: fairTypeValueEnum,
},
{
title: '举办区域',
dataIndex: 'region',
ellipsis: true,
hideInSearch: true,
valueType: 'select',
valueEnum: regionValueEnum,
},
{
title: '举办地址',
@@ -175,6 +215,7 @@ const OutdoorFairList: React.FC = () => {
title: params.title,
hostUnit: params.hostUnit,
fairType: params.fairType,
region: params.region,
} as OutdoorFairListParams);
return { data: res.rows, total: res.total, success: true };
}}
@@ -196,14 +237,15 @@ const OutdoorFairList: React.FC = () => {
<EditModal
open={modalVisible}
values={currentRow}
fairTypeOptions={fairTypeOptions}
regionOptions={regionOptions}
onCreateDictOption={handleCreateDictOption}
onCancel={() => {
setModalVisible(false);
setCurrentRow(undefined);
}}
onSubmit={async (values: OutdoorFairForm) => {
const res = values.id
? await updateOutdoorFair(values)
: await addOutdoorFair(values);
const res = values.id ? await updateOutdoorFair(values) : await addOutdoorFair(values);
if (res.code === 200) {
message.success(values.id ? '修改成功' : '新增成功');
setModalVisible(false);