feat: Enhance Outdoor Fair and Public Job Fair features
- Added QR code functionality for companies signing up for outdoor fairs. - Implemented job management for outdoor fairs, including job listing and editing. - Updated Public Job Fair detail view to reflect new data structure. - Refactored EditModal to support dynamic lists for host, co-organizer, and organizing units. - Introduced cross-domain city selection for job fairs. - Enhanced API services for managing cross-domain jobs and cities. - Improved data handling for outdoor fair items, including review status and QR code content. - Updated type definitions to accommodate new fields and structures.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -6,13 +6,13 @@ import {
|
||||
ProColumns,
|
||||
ProTable,
|
||||
ModalForm,
|
||||
ProFormDigit,
|
||||
ProFormText,
|
||||
ProFormSelect,
|
||||
} from '@ant-design/pro-components';
|
||||
import { PlusOutlined, DeleteOutlined, LinkOutlined, DisconnectOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
getOutdoorFairDeviceList,
|
||||
batchGenerateOutdoorFairDevice,
|
||||
addOutdoorFairDevice,
|
||||
bindOutdoorFairDevice,
|
||||
deleteOutdoorFairDevice,
|
||||
getOutdoorFairCompanyOptions,
|
||||
@@ -26,7 +26,7 @@ interface Props {
|
||||
const DeviceTab: React.FC<Props> = ({ fairId }) => {
|
||||
const access = useAccess();
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [batchVisible, setBatchVisible] = useState(false);
|
||||
const [addVisible, setAddVisible] = useState(false);
|
||||
const [bindVisible, setBindVisible] = useState(false);
|
||||
const [currentRow, setCurrentRow] = useState<OutdoorFairDeviceItem>();
|
||||
|
||||
@@ -145,13 +145,13 @@ const DeviceTab: React.FC<Props> = ({ fairId }) => {
|
||||
scroll={{ x: 'max-content' }}
|
||||
toolBarRender={() => [
|
||||
<Button
|
||||
key="batch"
|
||||
key="add"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
hidden={!access.hasPerms('cms:outdoorFair:add')}
|
||||
onClick={() => setBatchVisible(true)}
|
||||
onClick={() => setAddVisible(true)}
|
||||
>
|
||||
批量生成设备码
|
||||
新增设备码
|
||||
</Button>,
|
||||
]}
|
||||
request={async (params) => {
|
||||
@@ -167,34 +167,60 @@ const DeviceTab: React.FC<Props> = ({ fairId }) => {
|
||||
columns={columns}
|
||||
/>
|
||||
|
||||
<ModalForm<{ quantity: number }>
|
||||
title="批量生成设备码"
|
||||
open={batchVisible}
|
||||
modalProps={{ destroyOnClose: true, onCancel: () => setBatchVisible(false) }}
|
||||
initialValues={{ quantity: 10 }}
|
||||
<ModalForm<{ deviceCode: string; companyId?: number }>
|
||||
title="新增设备码"
|
||||
open={addVisible}
|
||||
modalProps={{ destroyOnClose: true, onCancel: () => setAddVisible(false) }}
|
||||
onFinish={async (values) => {
|
||||
const res = await batchGenerateOutdoorFairDevice({
|
||||
const res = await addOutdoorFairDevice({
|
||||
fairId,
|
||||
quantity: Number(values.quantity),
|
||||
deviceCode: values.deviceCode.trim(),
|
||||
companyId: values.companyId,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success(`成功生成 ${res.data?.length || values.quantity} 个设备码`);
|
||||
setBatchVisible(false);
|
||||
message.success('新增成功');
|
||||
setAddVisible(false);
|
||||
actionRef.current?.reload();
|
||||
return true;
|
||||
}
|
||||
message.error(res.msg || '生成失败');
|
||||
message.error(res.msg || '新增失败');
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<ProFormDigit
|
||||
name="quantity"
|
||||
label="生成数量"
|
||||
placeholder="请输入需要生成的设备码数量"
|
||||
min={1}
|
||||
max={1000}
|
||||
fieldProps={{ precision: 0 }}
|
||||
rules={[{ required: true, message: '请输入生成数量' }]}
|
||||
<ProFormText
|
||||
name="deviceCode"
|
||||
label="设备码"
|
||||
placeholder="请输入设备码"
|
||||
fieldProps={{ maxLength: 64 }}
|
||||
rules={[
|
||||
{ required: true, message: '请输入设备码' },
|
||||
{ max: 64, message: '设备码长度不能超过64个字符' },
|
||||
]}
|
||||
/>
|
||||
<ProFormSelect
|
||||
name="companyId"
|
||||
label="绑定企业"
|
||||
placeholder="选填,不选则创建未绑定的设备码"
|
||||
showSearch
|
||||
allowClear
|
||||
request={async () => {
|
||||
const [companies, devicesRes] = await Promise.all([
|
||||
getOutdoorFairCompanyOptions(fairId),
|
||||
getOutdoorFairDeviceList({ fairId, current: 1, pageSize: 1000 }),
|
||||
]);
|
||||
// 排除已绑定到其他设备的企业
|
||||
const boundIds = new Set(
|
||||
(devicesRes.rows || [])
|
||||
.map((d) => d.companyId)
|
||||
.filter((cid): cid is number => cid != null),
|
||||
);
|
||||
return companies
|
||||
.filter((item) => !boundIds.has(item.companyId))
|
||||
.map((item) => ({
|
||||
label: item.companyName,
|
||||
value: item.companyId,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</ModalForm>
|
||||
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
import { Button, Form, Input, message } from 'antd';
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import type { DictValueEnumObj } from '@/components/DictTag';
|
||||
import { addJobForCompany, updateJobForCompany } from '@/services/jobportal/outdoorFairDetail';
|
||||
import {
|
||||
addCurrentCompanyJob,
|
||||
addJobForCompany,
|
||||
updateJobForCompany,
|
||||
} from '@/services/jobportal/outdoorFairDetail';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@@ -21,6 +25,7 @@ interface Props {
|
||||
experienceEnum: DictValueEnumObj;
|
||||
areaEnum: DictValueEnumObj;
|
||||
jobTypeEnum: DictValueEnumObj;
|
||||
companyScoped?: boolean;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
@@ -34,6 +39,7 @@ const JobEditModal: React.FC<Props> = ({
|
||||
experienceEnum,
|
||||
areaEnum,
|
||||
jobTypeEnum,
|
||||
companyScoped,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}) => {
|
||||
@@ -82,7 +88,9 @@ const JobEditModal: React.FC<Props> = ({
|
||||
};
|
||||
const res = job
|
||||
? await updateJobForCompany(fairId, { ...job, ...payload })
|
||||
: await addJobForCompany({ fairId, companyId: company!.companyId, ...payload });
|
||||
: companyScoped
|
||||
? await addCurrentCompanyJob({ fairId, ...payload })
|
||||
: await addJobForCompany({ fairId, companyId: company!.companyId, ...payload });
|
||||
if (res.code === 200) {
|
||||
message.success(job ? '岗位修改成功' : '岗位添加成功');
|
||||
onSuccess();
|
||||
@@ -163,7 +171,13 @@ const JobEditModal: React.FC<Props> = ({
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<div
|
||||
key={key}
|
||||
style={{ display: 'flex', width: '100%', marginBottom: 8, alignItems: 'center', gap: 8 }}
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
marginBottom: 8,
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
@@ -195,7 +209,12 @@ const JobEditModal: React.FC<Props> = ({
|
||||
>
|
||||
<Input placeholder="请输入职务" />
|
||||
</Form.Item>
|
||||
<Button type="text" danger icon={<MinusCircleOutlined />} onClick={() => remove(name)} />
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<MinusCircleOutlined />}
|
||||
onClick={() => remove(name)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<Form.Item>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Button, Modal, Tag, message } from 'antd';
|
||||
import { Button, Form, Input, Modal, QRCode, Select, Tag, Typography, message } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import type { ActionType, ProColumns } from '@ant-design/pro-components';
|
||||
import { ProTable } from '@ant-design/pro-components';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
import {
|
||||
getParticipatingCompanyQrCode,
|
||||
getParticipatingCompanies,
|
||||
getParticipatingJobDetail,
|
||||
getParticipatingJobs,
|
||||
removeJobFromCompany,
|
||||
removeParticipatingCompany,
|
||||
reviewParticipatingCompany,
|
||||
reviewParticipatingJob,
|
||||
} from '@/services/jobportal/outdoorFairDetail';
|
||||
import CompanyAddModal from './CompanyAddModal';
|
||||
import JobEditModal from './JobEditModal';
|
||||
@@ -18,20 +21,46 @@ interface Props {
|
||||
fairId: number;
|
||||
}
|
||||
|
||||
type ReviewTarget =
|
||||
| {
|
||||
type: 'company';
|
||||
record: API.OutdoorFairDetail.ParticipatingCompany;
|
||||
}
|
||||
| {
|
||||
type: 'job';
|
||||
record: API.OutdoorFairDetail.PostedJob;
|
||||
};
|
||||
|
||||
const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
const companyActionRef = useRef<ActionType>();
|
||||
const jobActionRef = useRef<ActionType>();
|
||||
const [reviewForm] = Form.useForm();
|
||||
const [companyModalOpen, setCompanyModalOpen] = useState(false);
|
||||
const [jobListModalOpen, setJobListModalOpen] = useState(false);
|
||||
const [jobEditModalOpen, setJobEditModalOpen] = useState(false);
|
||||
const [selectedCompany, setSelectedCompany] =
|
||||
useState<API.OutdoorFairDetail.ParticipatingCompany | null>(null);
|
||||
const [qrModalOpen, setQrModalOpen] = useState(false);
|
||||
const [qrInfo, setQrInfo] = useState<API.OutdoorFairDetail.CompanyQrCodeInfo | null>(null);
|
||||
const [qrCompanyName, setQrCompanyName] = useState('');
|
||||
const [editingJob, setEditingJob] = useState<API.OutdoorFairDetail.PostedJob | null>(null);
|
||||
const [educationEnum, setEducationEnum] = useState<any>({});
|
||||
const [experienceEnum, setExperienceEnum] = useState<any>({});
|
||||
const [areaEnum, setAreaEnum] = useState<any>({});
|
||||
const [jobTypeEnum, setJobTypeEnum] = useState<any>({});
|
||||
const [scaleEnum, setScaleEnum] = useState<any>({});
|
||||
const [reviewModalOpen, setReviewModalOpen] = useState(false);
|
||||
const [reviewTarget, setReviewTarget] = useState<ReviewTarget | null>(null);
|
||||
|
||||
const renderReviewStatus = (status?: string) => {
|
||||
const map: Record<string, { color: string; text: string }> = {
|
||||
'0': { color: 'processing', text: '待审核' },
|
||||
'1': { color: 'success', text: '已通过' },
|
||||
'2': { color: 'error', text: '已驳回' },
|
||||
};
|
||||
const item = map[status || '1'] || map['1'];
|
||||
return <Tag color={item.color}>{item.text}</Tag>;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
@@ -63,6 +92,67 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
setJobEditModalOpen(true);
|
||||
};
|
||||
|
||||
const openQrCodeModal = async (record: API.OutdoorFairDetail.ParticipatingCompany) => {
|
||||
const res = await getParticipatingCompanyQrCode(fairId, record.companyId);
|
||||
if (res.code !== 200) {
|
||||
message.error(res.msg || '二维码内容获取失败');
|
||||
return;
|
||||
}
|
||||
setQrInfo(res.data);
|
||||
setQrCompanyName(record.companyName);
|
||||
setQrModalOpen(true);
|
||||
};
|
||||
|
||||
const openReviewModal = (target: ReviewTarget) => {
|
||||
setReviewTarget(target);
|
||||
reviewForm.setFieldsValue({
|
||||
reviewStatus:
|
||||
target.type === 'company'
|
||||
? target.record.reviewStatus || '0'
|
||||
: target.record.fairReviewStatus || '0',
|
||||
reviewRemark:
|
||||
target.type === 'company'
|
||||
? target.record.reviewRemark || ''
|
||||
: target.record.fairReviewRemark || '',
|
||||
});
|
||||
setReviewModalOpen(true);
|
||||
};
|
||||
|
||||
const submitReview = async () => {
|
||||
const values = await reviewForm.validateFields();
|
||||
if (!reviewTarget) return;
|
||||
const payload = {
|
||||
reviewStatus: values.reviewStatus as '0' | '1' | '2',
|
||||
reviewRemark: values.reviewRemark,
|
||||
};
|
||||
const res =
|
||||
reviewTarget.type === 'company'
|
||||
? await reviewParticipatingCompany({
|
||||
fairId,
|
||||
id: reviewTarget.record.id,
|
||||
...payload,
|
||||
})
|
||||
: await reviewParticipatingJob({
|
||||
fairId,
|
||||
jobId: reviewTarget.record.jobId!,
|
||||
...payload,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success('审核成功');
|
||||
setReviewModalOpen(false);
|
||||
setReviewTarget(null);
|
||||
reviewForm.resetFields();
|
||||
if (reviewTarget.type === 'company') {
|
||||
companyActionRef.current?.reload();
|
||||
} else {
|
||||
jobActionRef.current?.reload();
|
||||
companyActionRef.current?.reload();
|
||||
}
|
||||
} else {
|
||||
message.error(res.msg || '审核失败');
|
||||
}
|
||||
};
|
||||
|
||||
const jobColumns: ProColumns<API.OutdoorFairDetail.PostedJob>[] = [
|
||||
{ title: '岗位名称', dataIndex: 'jobTitle', ellipsis: true },
|
||||
{
|
||||
@@ -85,6 +175,13 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
width: 110,
|
||||
},
|
||||
{ title: '招聘人数', dataIndex: 'vacancies', hideInSearch: true, width: 90 },
|
||||
{
|
||||
title: '审核状态',
|
||||
dataIndex: 'fairReviewStatus',
|
||||
hideInSearch: true,
|
||||
width: 100,
|
||||
render: (_, record) => renderReviewStatus(record.fairReviewStatus),
|
||||
},
|
||||
{
|
||||
title: '发布状态',
|
||||
dataIndex: 'isPublish',
|
||||
@@ -99,11 +196,19 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
width: 130,
|
||||
width: 240,
|
||||
render: (_, record) => [
|
||||
<Button key="edit" type="link" size="small" onClick={() => openJobEditor(record)}>
|
||||
编辑
|
||||
</Button>,
|
||||
<Button
|
||||
key="review"
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => openReviewModal({ type: 'job', record })}
|
||||
>
|
||||
审核
|
||||
</Button>,
|
||||
<Button
|
||||
key="delete"
|
||||
type="link"
|
||||
@@ -140,7 +245,12 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
options={false}
|
||||
pagination={{ pageSize: 10 }}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => setCompanyModalOpen(true)}>
|
||||
<Button
|
||||
key="add"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setCompanyModalOpen(true)}
|
||||
>
|
||||
添加企业
|
||||
</Button>,
|
||||
]}
|
||||
@@ -180,6 +290,12 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
},
|
||||
{ title: '联系人', dataIndex: 'contactPerson', hideInSearch: true },
|
||||
{ title: '联系电话', dataIndex: 'contactPhone', hideInSearch: true },
|
||||
{
|
||||
title: '审核状态',
|
||||
dataIndex: 'reviewStatus',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => renderReviewStatus(record.reviewStatus),
|
||||
},
|
||||
{
|
||||
title: '展位号',
|
||||
dataIndex: 'boothNumber',
|
||||
@@ -190,7 +306,7 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
width: 220,
|
||||
width: 460,
|
||||
render: (_, record) => [
|
||||
<Button
|
||||
key="jobs"
|
||||
@@ -214,6 +330,17 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
>
|
||||
添加岗位
|
||||
</Button>,
|
||||
<Button key="qrcode" type="link" size="small" onClick={() => openQrCodeModal(record)}>
|
||||
查看二维码
|
||||
</Button>,
|
||||
<Button
|
||||
key="review"
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => openReviewModal({ type: 'company', record })}
|
||||
>
|
||||
审核
|
||||
</Button>,
|
||||
<Button
|
||||
key="remove"
|
||||
type="link"
|
||||
@@ -256,7 +383,12 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
options={false}
|
||||
pagination={{ pageSize: 10 }}
|
||||
toolBarRender={() => [
|
||||
<Button key="add" type="primary" icon={<PlusOutlined />} onClick={() => openJobEditor()}>
|
||||
<Button
|
||||
key="add"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => openJobEditor()}
|
||||
>
|
||||
添加岗位
|
||||
</Button>,
|
||||
]}
|
||||
@@ -276,6 +408,97 @@ const ParticipatingCompaniesTab: React.FC<Props> = ({ fairId }) => {
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={qrCompanyName ? `${qrCompanyName} — 企业二维码` : '企业二维码'}
|
||||
open={qrModalOpen}
|
||||
width={420}
|
||||
footer={[
|
||||
<Button
|
||||
key="open"
|
||||
type="primary"
|
||||
onClick={() => qrInfo?.h5Url && window.open(qrInfo.h5Url)}
|
||||
>
|
||||
打开链接
|
||||
</Button>,
|
||||
]}
|
||||
destroyOnClose
|
||||
onCancel={() => {
|
||||
setQrModalOpen(false);
|
||||
setQrInfo(null);
|
||||
setQrCompanyName('');
|
||||
}}
|
||||
>
|
||||
{qrInfo?.qrCodeContent && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16 }}>
|
||||
<QRCode value={qrInfo.qrCodeContent} size={220} />
|
||||
<Typography.Paragraph
|
||||
copyable={{ text: qrInfo.qrCodeContent }}
|
||||
style={{
|
||||
width: '100%',
|
||||
marginBottom: 0,
|
||||
wordBreak: 'break-all',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{qrInfo.qrCodeContent}
|
||||
</Typography.Paragraph>
|
||||
{!qrInfo.deviceCode && (
|
||||
<Typography.Text type="secondary">
|
||||
当前企业未绑定设备码,二维码使用企业公共页链接。
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={
|
||||
reviewTarget?.type === 'company'
|
||||
? `审核报名 — ${reviewTarget.record.companyName}`
|
||||
: `审核岗位 — ${reviewTarget?.record.jobTitle || ''}`
|
||||
}
|
||||
open={reviewModalOpen}
|
||||
destroyOnClose
|
||||
onCancel={() => {
|
||||
setReviewModalOpen(false);
|
||||
setReviewTarget(null);
|
||||
reviewForm.resetFields();
|
||||
}}
|
||||
onOk={submitReview}
|
||||
>
|
||||
<Form form={reviewForm} layout="vertical">
|
||||
<Form.Item
|
||||
name="reviewStatus"
|
||||
label="审核状态"
|
||||
rules={[{ required: true, message: '请选择审核状态' }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: '待审核', value: '0' },
|
||||
{ label: '已通过', value: '1' },
|
||||
{ label: '已驳回', value: '2' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.reviewStatus !== cur.reviewStatus}>
|
||||
{({ getFieldValue }) => (
|
||||
<Form.Item
|
||||
name="reviewRemark"
|
||||
label="审核原因"
|
||||
rules={[
|
||||
{
|
||||
required: getFieldValue('reviewStatus') === '2',
|
||||
message: '驳回时请填写原因',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="驳回时请输入原因" maxLength={500} showCount />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<CompanyAddModal
|
||||
open={companyModalOpen}
|
||||
fairId={fairId}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Button, Modal, Tag, message } from 'antd';
|
||||
import { Button, Form, Input, Modal, Select, Tag, message } from 'antd';
|
||||
import type { ActionType, ProColumns } from '@ant-design/pro-components';
|
||||
import { ProTable } from '@ant-design/pro-components';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getParticipatingJobDetail,
|
||||
getParticipatingJobs,
|
||||
removeJobFromCompany,
|
||||
reviewParticipatingJob,
|
||||
} from '@/services/jobportal/outdoorFairDetail';
|
||||
import JobEditModal from './JobEditModal';
|
||||
|
||||
@@ -17,15 +18,28 @@ interface Props {
|
||||
|
||||
const ParticipatingJobsTab: React.FC<Props> = ({ fairId }) => {
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [reviewForm] = Form.useForm();
|
||||
const [editingJob, setEditingJob] = useState<API.OutdoorFairDetail.PostedJob | null>(null);
|
||||
const [editingCompany, setEditingCompany] =
|
||||
useState<API.OutdoorFairDetail.ParticipatingCompany | null>(null);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [reviewModalOpen, setReviewModalOpen] = useState(false);
|
||||
const [reviewingJob, setReviewingJob] = useState<API.OutdoorFairDetail.PostedJob | null>(null);
|
||||
const [educationEnum, setEducationEnum] = useState<any>({});
|
||||
const [experienceEnum, setExperienceEnum] = useState<any>({});
|
||||
const [areaEnum, setAreaEnum] = useState<any>({});
|
||||
const [jobTypeEnum, setJobTypeEnum] = useState<any>({});
|
||||
|
||||
const renderReviewStatus = (status?: string) => {
|
||||
const map: Record<string, { color: string; text: string }> = {
|
||||
'0': { color: 'processing', text: '待审核' },
|
||||
'1': { color: 'success', text: '已通过' },
|
||||
'2': { color: 'error', text: '已驳回' },
|
||||
};
|
||||
const item = map[status || '1'] || map['1'];
|
||||
return <Tag color={item.color}>{item.text}</Tag>;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
getDictValueEnum('education', true, true),
|
||||
@@ -61,6 +75,35 @@ const ParticipatingJobsTab: React.FC<Props> = ({ fairId }) => {
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const openReviewModal = (record: API.OutdoorFairDetail.PostedJob) => {
|
||||
setReviewingJob(record);
|
||||
reviewForm.setFieldsValue({
|
||||
reviewStatus: record.fairReviewStatus || '0',
|
||||
reviewRemark: record.fairReviewRemark || '',
|
||||
});
|
||||
setReviewModalOpen(true);
|
||||
};
|
||||
|
||||
const submitReview = async () => {
|
||||
const values = await reviewForm.validateFields();
|
||||
if (!reviewingJob) return;
|
||||
const res = await reviewParticipatingJob({
|
||||
fairId,
|
||||
jobId: reviewingJob.jobId!,
|
||||
reviewStatus: values.reviewStatus,
|
||||
reviewRemark: values.reviewRemark,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success('审核成功');
|
||||
setReviewModalOpen(false);
|
||||
setReviewingJob(null);
|
||||
reviewForm.resetFields();
|
||||
actionRef.current?.reload();
|
||||
} else {
|
||||
message.error(res.msg || '审核失败');
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ProColumns<API.OutdoorFairDetail.PostedJob>[] = [
|
||||
{ title: '岗位名称', dataIndex: 'jobTitle', ellipsis: true },
|
||||
{
|
||||
@@ -111,6 +154,13 @@ const ParticipatingJobsTab: React.FC<Props> = ({ fairId }) => {
|
||||
},
|
||||
{ title: '招聘人数', dataIndex: 'vacancies', hideInSearch: true, width: 90 },
|
||||
{ title: '工作地点', dataIndex: 'jobLocation', hideInSearch: true, ellipsis: true },
|
||||
{
|
||||
title: '审核状态',
|
||||
dataIndex: 'fairReviewStatus',
|
||||
hideInSearch: true,
|
||||
width: 100,
|
||||
render: (_, record) => renderReviewStatus(record.fairReviewStatus),
|
||||
},
|
||||
{
|
||||
title: '发布状态',
|
||||
dataIndex: 'isPublish',
|
||||
@@ -125,11 +175,14 @@ const ParticipatingJobsTab: React.FC<Props> = ({ fairId }) => {
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
width: 130,
|
||||
width: 240,
|
||||
render: (_, record) => [
|
||||
<Button key="edit" type="link" size="small" onClick={() => editJob(record)}>
|
||||
编辑
|
||||
</Button>,
|
||||
<Button key="review" type="link" size="small" onClick={() => openReviewModal(record)}>
|
||||
审核
|
||||
</Button>,
|
||||
<Button
|
||||
key="delete"
|
||||
type="link"
|
||||
@@ -193,6 +246,49 @@ const ParticipatingJobsTab: React.FC<Props> = ({ fairId }) => {
|
||||
actionRef.current?.reload();
|
||||
}}
|
||||
/>
|
||||
<Modal
|
||||
title={`审核岗位 — ${reviewingJob?.jobTitle || ''}`}
|
||||
open={reviewModalOpen}
|
||||
destroyOnClose
|
||||
onCancel={() => {
|
||||
setReviewModalOpen(false);
|
||||
setReviewingJob(null);
|
||||
reviewForm.resetFields();
|
||||
}}
|
||||
onOk={submitReview}
|
||||
>
|
||||
<Form form={reviewForm} layout="vertical">
|
||||
<Form.Item
|
||||
name="reviewStatus"
|
||||
label="审核状态"
|
||||
rules={[{ required: true, message: '请选择审核状态' }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: '待审核', value: '0' },
|
||||
{ label: '已通过', value: '1' },
|
||||
{ label: '已驳回', value: '2' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.reviewStatus !== cur.reviewStatus}>
|
||||
{({ getFieldValue }) => (
|
||||
<Form.Item
|
||||
name="reviewRemark"
|
||||
label="审核原因"
|
||||
rules={[
|
||||
{
|
||||
required: getFieldValue('reviewStatus') === '2',
|
||||
message: '驳回时请填写原因',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="驳回时请输入原因" maxLength={500} showCount />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,38 +1,74 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useAccess, history } from '@umijs/max';
|
||||
import { Button, message, Modal } from 'antd';
|
||||
import { useAccess, history, useModel } from '@umijs/max';
|
||||
import { Button, message, Modal, QRCode, Tag, Typography } from 'antd';
|
||||
import { ActionType, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import { PlusOutlined, DeleteOutlined, FormOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
FormOutlined,
|
||||
EyeOutlined,
|
||||
QrcodeOutlined,
|
||||
UnorderedListOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
getOutdoorFairList,
|
||||
addOutdoorFair,
|
||||
updateOutdoorFair,
|
||||
deleteOutdoorFair,
|
||||
addOutdoorFairDictOption,
|
||||
signupOutdoorFair,
|
||||
getMyOutdoorFairQrCode,
|
||||
} from '@/services/jobportal/outdoorFair';
|
||||
import type {
|
||||
OutdoorFairItem,
|
||||
OutdoorFairListParams,
|
||||
OutdoorFairForm,
|
||||
OutdoorFairDictForm,
|
||||
OutdoorFairCompanyQrCodeInfo,
|
||||
} from '@/services/jobportal/outdoorFair';
|
||||
import { getParticipatingJobs } from '@/services/jobportal/outdoorFairDetail';
|
||||
import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict';
|
||||
import { getVenueInfoList } from '@/services/jobportal/venueInfo';
|
||||
import EditModal from './components/EditModal';
|
||||
import JobEditModal from './Detail/components/JobEditModal';
|
||||
|
||||
const OUTDOOR_FAIR_TYPE_DICT = 'outdoor_fair_type';
|
||||
const OUTDOOR_FAIR_REGION_DICT = 'outdoor_fair_region';
|
||||
|
||||
const OutdoorFairList: React.FC = () => {
|
||||
const access = useAccess();
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const isEnterprise = initialState?.currentUser?.userType === 'view';
|
||||
const actionRef = useRef<ActionType>();
|
||||
const jobActionRef = useRef<ActionType>();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [currentRow, setCurrentRow] = useState<OutdoorFairItem>();
|
||||
const [selectedFair, setSelectedFair] = useState<OutdoorFairItem | null>(null);
|
||||
const [jobListModalOpen, setJobListModalOpen] = useState(false);
|
||||
const [jobEditModalOpen, setJobEditModalOpen] = useState(false);
|
||||
const [qrModalOpen, setQrModalOpen] = useState(false);
|
||||
const [qrInfo, setQrInfo] = useState<OutdoorFairCompanyQrCodeInfo | null>(null);
|
||||
const [rejectReasonModalOpen, setRejectReasonModalOpen] = useState(false);
|
||||
const [rejectReason, setRejectReason] = useState('');
|
||||
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 [venueOptions, setVenueOptions] = useState<any[]>([]);
|
||||
const [educationEnum, setEducationEnum] = useState<any>({});
|
||||
const [experienceEnum, setExperienceEnum] = useState<any>({});
|
||||
const [areaEnum, setAreaEnum] = useState<any>({});
|
||||
const [jobTypeEnum, setJobTypeEnum] = useState<any>({});
|
||||
|
||||
const renderReviewStatus = (status?: string) => {
|
||||
const map: Record<string, { color: string; text: string }> = {
|
||||
'0': { color: 'processing', text: '待审核' },
|
||||
'1': { color: 'success', text: '已通过' },
|
||||
'2': { color: 'error', text: '已驳回' },
|
||||
};
|
||||
const item = map[status || '0'] || map['0'];
|
||||
return <Tag color={item.color}>{item.text}</Tag>;
|
||||
};
|
||||
|
||||
const refreshDictOptions = useCallback(async () => {
|
||||
const [typeValueEnum, typeOptions, regionEnum, regionSelectOptions] = await Promise.all([
|
||||
@@ -51,6 +87,20 @@ const OutdoorFairList: React.FC = () => {
|
||||
refreshDictOptions();
|
||||
}, [refreshDictOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
getDictValueEnum('education', true, true),
|
||||
getDictValueEnum('experience', true, true),
|
||||
getDictValueEnum('area', true, true),
|
||||
getDictValueEnum('job_type', true, true),
|
||||
]).then(([education, experience, area, jobType]) => {
|
||||
setEducationEnum(education);
|
||||
setExperienceEnum(experience);
|
||||
setAreaEnum(area);
|
||||
setJobTypeEnum(jobType);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const refreshVenueOptions = useCallback(async () => {
|
||||
const res = await getVenueInfoList({ current: 1, pageSize: 999 });
|
||||
if (res.code === 200) {
|
||||
@@ -66,8 +116,10 @@ const OutdoorFairList: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refreshVenueOptions();
|
||||
}, [refreshVenueOptions]);
|
||||
if (!isEnterprise) {
|
||||
refreshVenueOptions();
|
||||
}
|
||||
}, [isEnterprise, refreshVenueOptions]);
|
||||
|
||||
const handleCreateDictOption = async (values: OutdoorFairDictForm) => {
|
||||
const res = await addOutdoorFairDictOption(values);
|
||||
@@ -96,6 +148,87 @@ const OutdoorFairList: React.FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const getCurrentCompany = (
|
||||
record: OutdoorFairItem,
|
||||
): API.OutdoorFairDetail.ParticipatingCompany => ({
|
||||
id: 0,
|
||||
fairId: record.id,
|
||||
companyId: record.companyId!,
|
||||
companyName: record.companyName || '本企业',
|
||||
industry: '',
|
||||
scale: '',
|
||||
contactPerson: '',
|
||||
contactPhone: '',
|
||||
jobCount: 0,
|
||||
});
|
||||
|
||||
const handleSignup = async (record: OutdoorFairItem) => {
|
||||
const res = await signupOutdoorFair(record.id);
|
||||
if (res.code === 200) {
|
||||
message.success('报名成功,等待审核');
|
||||
actionRef.current?.reload();
|
||||
} else {
|
||||
message.error(res.msg || '报名失败');
|
||||
}
|
||||
};
|
||||
|
||||
const openQrCode = async (record: OutdoorFairItem) => {
|
||||
const res = await getMyOutdoorFairQrCode(record.id);
|
||||
if (res.code === 200) {
|
||||
setQrInfo(res.data);
|
||||
setQrModalOpen(true);
|
||||
} else {
|
||||
message.error(res.msg || '二维码加载失败');
|
||||
}
|
||||
};
|
||||
|
||||
const openJobList = (record: OutdoorFairItem) => {
|
||||
setSelectedFair(record);
|
||||
setJobListModalOpen(true);
|
||||
};
|
||||
|
||||
const openJobEditor = (record: OutdoorFairItem) => {
|
||||
setSelectedFair(record);
|
||||
setJobEditModalOpen(true);
|
||||
};
|
||||
|
||||
const openRejectReason = (record: OutdoorFairItem) => {
|
||||
setRejectReason(record.reviewRemark || '暂无驳回原因');
|
||||
setRejectReasonModalOpen(true);
|
||||
};
|
||||
|
||||
const jobColumns: ProColumns<API.OutdoorFairDetail.PostedJob>[] = [
|
||||
{ title: '岗位名称', dataIndex: 'jobTitle', ellipsis: true },
|
||||
{
|
||||
title: '薪资范围',
|
||||
hideInSearch: true,
|
||||
render: (_, record) => `${record.minSalary}-${record.maxSalary} 元/月`,
|
||||
},
|
||||
{
|
||||
title: '学历要求',
|
||||
dataIndex: 'education',
|
||||
valueType: 'select',
|
||||
valueEnum: educationEnum,
|
||||
width: 110,
|
||||
},
|
||||
{
|
||||
title: '工作经验',
|
||||
dataIndex: 'experience',
|
||||
valueType: 'select',
|
||||
valueEnum: experienceEnum,
|
||||
width: 110,
|
||||
},
|
||||
{ title: '招聘人数', dataIndex: 'vacancies', hideInSearch: true, width: 90 },
|
||||
{
|
||||
title: '审核状态',
|
||||
dataIndex: 'fairReviewStatus',
|
||||
hideInSearch: true,
|
||||
width: 100,
|
||||
render: (_, record) => renderReviewStatus(record.fairReviewStatus),
|
||||
},
|
||||
{ title: '工作地点', dataIndex: 'jobLocation', hideInSearch: true, ellipsis: true },
|
||||
];
|
||||
|
||||
const columns: ProColumns<OutdoorFairItem>[] = [
|
||||
{
|
||||
title: '招聘会标题',
|
||||
@@ -122,7 +255,7 @@ const OutdoorFairList: React.FC = () => {
|
||||
valueEnum: regionValueEnum,
|
||||
},
|
||||
{
|
||||
title: '绑定场地',
|
||||
title: '场地名称',
|
||||
dataIndex: 'venueName',
|
||||
ellipsis: true,
|
||||
valueType: 'select',
|
||||
@@ -135,18 +268,7 @@ const OutdoorFairList: React.FC = () => {
|
||||
search: {
|
||||
transform: (value) => ({ venueId: value }),
|
||||
},
|
||||
render: (_, record) =>
|
||||
record.venueId && record.venueName ? (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => history.push(`/jobfair/venue-info/detail?id=${record.venueId}`)}
|
||||
>
|
||||
{record.venueName}
|
||||
</Button>
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
render: (_, record) => record.venueName || '--',
|
||||
},
|
||||
{
|
||||
title: '举办地址',
|
||||
@@ -188,11 +310,29 @@ const OutdoorFairList: React.FC = () => {
|
||||
dataIndex: 'onlineApply',
|
||||
valueType: 'select',
|
||||
valueEnum: {
|
||||
'true': { text: '是' },
|
||||
'false': { text: '否' },
|
||||
true: { text: '是' },
|
||||
false: { text: '否' },
|
||||
},
|
||||
render: (_, record) => (record.onlineApply ? '是' : '否'),
|
||||
},
|
||||
{
|
||||
title: '报名审核状态',
|
||||
dataIndex: 'reviewStatus',
|
||||
hideInSearch: true,
|
||||
hideInTable: !isEnterprise,
|
||||
render: (_, record) =>
|
||||
record.signedUp ? (
|
||||
record.reviewStatus === '2' ? (
|
||||
<Button type="link" size="small" danger onClick={() => openRejectReason(record)}>
|
||||
{renderReviewStatus(record.reviewStatus)}
|
||||
</Button>
|
||||
) : (
|
||||
renderReviewStatus(record.reviewStatus)
|
||||
)
|
||||
) : (
|
||||
'--'
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '招聘会照片',
|
||||
dataIndex: 'photoUrl',
|
||||
@@ -211,14 +351,47 @@ const OutdoorFairList: React.FC = () => {
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
width: 160,
|
||||
width: isEnterprise ? 280 : 160,
|
||||
fixed: 'right',
|
||||
render: (_, record) => [
|
||||
isEnterprise && !record.signedUp ? (
|
||||
<Button key="signup" type="link" size="small" onClick={() => handleSignup(record)}>
|
||||
报名
|
||||
</Button>
|
||||
) : null,
|
||||
isEnterprise && record.signedUp && record.reviewStatus === '1' ? (
|
||||
<Button
|
||||
key="qr"
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<QrcodeOutlined />}
|
||||
onClick={() => openQrCode(record)}
|
||||
>
|
||||
查看二维码
|
||||
</Button>
|
||||
) : null,
|
||||
isEnterprise && record.signedUp && record.reviewStatus === '1' ? (
|
||||
<Button key="addJob" type="link" size="small" onClick={() => openJobEditor(record)}>
|
||||
添加岗位
|
||||
</Button>
|
||||
) : null,
|
||||
isEnterprise && record.signedUp && record.reviewStatus === '1' ? (
|
||||
<Button
|
||||
key="jobs"
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<UnorderedListOutlined />}
|
||||
onClick={() => openJobList(record)}
|
||||
>
|
||||
查看报名岗位
|
||||
</Button>
|
||||
) : null,
|
||||
<Button
|
||||
key="detail"
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
hidden={isEnterprise}
|
||||
onClick={() => history.push(`/jobfair/outdoor-fair/detail?id=${record.id}`)}
|
||||
>
|
||||
详情
|
||||
@@ -228,7 +401,7 @@ const OutdoorFairList: React.FC = () => {
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<FormOutlined />}
|
||||
hidden={!access.hasPerms('cms:outdoorFair:edit')}
|
||||
hidden={isEnterprise || !access.hasPerms('cms:outdoorFair:edit')}
|
||||
onClick={() => {
|
||||
setCurrentRow(record);
|
||||
setModalVisible(true);
|
||||
@@ -242,7 +415,7 @@ const OutdoorFairList: React.FC = () => {
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
hidden={!access.hasPerms('cms:outdoorFair:remove')}
|
||||
hidden={isEnterprise || !access.hasPerms('cms:outdoorFair:remove')}
|
||||
onClick={() => handleDelete(record.id)}
|
||||
>
|
||||
删除
|
||||
@@ -277,7 +450,7 @@ const OutdoorFairList: React.FC = () => {
|
||||
key="add"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
hidden={!access.hasPerms('cms:outdoorFair:add')}
|
||||
hidden={isEnterprise || !access.hasPerms('cms:outdoorFair:add')}
|
||||
onClick={() => {
|
||||
setCurrentRow(undefined);
|
||||
setModalVisible(true);
|
||||
@@ -310,6 +483,115 @@ const OutdoorFairList: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Modal
|
||||
title="企业二维码"
|
||||
open={qrModalOpen}
|
||||
width={420}
|
||||
footer={[
|
||||
<Button
|
||||
key="open"
|
||||
type="primary"
|
||||
onClick={() => qrInfo?.h5Url && window.open(qrInfo.h5Url)}
|
||||
>
|
||||
打开链接
|
||||
</Button>,
|
||||
]}
|
||||
destroyOnClose
|
||||
onCancel={() => {
|
||||
setQrModalOpen(false);
|
||||
setQrInfo(null);
|
||||
}}
|
||||
>
|
||||
{qrInfo?.qrCodeContent && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16 }}>
|
||||
<QRCode value={qrInfo.qrCodeContent} size={220} />
|
||||
<Typography.Paragraph
|
||||
copyable={{ text: qrInfo.qrCodeContent }}
|
||||
style={{
|
||||
width: '100%',
|
||||
marginBottom: 0,
|
||||
wordBreak: 'break-all',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{qrInfo.qrCodeContent}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
<Modal
|
||||
title="驳回原因"
|
||||
open={rejectReasonModalOpen}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
onCancel={() => {
|
||||
setRejectReasonModalOpen(false);
|
||||
setRejectReason('');
|
||||
}}
|
||||
>
|
||||
<Typography.Paragraph style={{ marginBottom: 0, whiteSpace: 'pre-wrap' }}>
|
||||
{rejectReason}
|
||||
</Typography.Paragraph>
|
||||
</Modal>
|
||||
<Modal
|
||||
title={selectedFair ? `${selectedFair.title} - 报名岗位` : '报名岗位'}
|
||||
open={jobListModalOpen}
|
||||
width={960}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
onCancel={() => {
|
||||
setJobListModalOpen(false);
|
||||
setSelectedFair(null);
|
||||
}}
|
||||
>
|
||||
{selectedFair && (
|
||||
<ProTable<API.OutdoorFairDetail.PostedJob>
|
||||
actionRef={jobActionRef}
|
||||
rowKey="jobId"
|
||||
options={false}
|
||||
search={{ labelWidth: 'auto', defaultCollapsed: false }}
|
||||
pagination={{ pageSize: 10 }}
|
||||
toolBarRender={() => [
|
||||
<Button
|
||||
key="add"
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => openJobEditor(selectedFair)}
|
||||
>
|
||||
添加岗位
|
||||
</Button>,
|
||||
]}
|
||||
request={async (params) => {
|
||||
const res = await getParticipatingJobs(selectedFair.id, {
|
||||
current: params.current,
|
||||
pageSize: params.pageSize,
|
||||
jobTitle: params.jobTitle,
|
||||
education: params.education,
|
||||
experience: params.experience,
|
||||
});
|
||||
return { data: res.rows || [], total: res.total || 0, success: res.code === 200 };
|
||||
}}
|
||||
columns={jobColumns}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
<JobEditModal
|
||||
open={jobEditModalOpen}
|
||||
fairId={selectedFair?.id || 0}
|
||||
company={selectedFair ? getCurrentCompany(selectedFair) : null}
|
||||
job={null}
|
||||
companyScoped
|
||||
educationEnum={educationEnum}
|
||||
experienceEnum={experienceEnum}
|
||||
areaEnum={areaEnum}
|
||||
jobTypeEnum={jobTypeEnum}
|
||||
onCancel={() => setJobEditModalOpen(false)}
|
||||
onSuccess={() => {
|
||||
setJobEditModalOpen(false);
|
||||
jobActionRef.current?.reload();
|
||||
actionRef.current?.reload();
|
||||
}}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -102,8 +102,11 @@ const JobFairDetail: React.FC = () => {
|
||||
{detail?.jobFairSignUpStartTime} ~ {detail?.jobFairSignUpEndTime}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="主办单位">{detail?.jobFairHostUnit}</Descriptions.Item>
|
||||
<Descriptions.Item label="协办单位">{detail?.jobFairCoOrganizer}</Descriptions.Item>
|
||||
<Descriptions.Item label="承办单位" span={2}>{detail?.jobFairOrganizer}</Descriptions.Item>
|
||||
<Descriptions.Item label="协办单位">{detail?.jobFairHelpUnit}</Descriptions.Item>
|
||||
<Descriptions.Item label="承办单位">{detail?.jobFairOrganizeUnit}</Descriptions.Item>
|
||||
<Descriptions.Item label="跨域联盟城市" span={2}>
|
||||
{detail?.crossDomainCities || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="简介" span={2}>{detail?.jobFairIntroduction}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
@@ -6,9 +6,13 @@ import {
|
||||
ProFormTextArea,
|
||||
ProFormRadio,
|
||||
ProFormDateTimePicker,
|
||||
ProFormList,
|
||||
ProFormSelect,
|
||||
} from '@ant-design/pro-components';
|
||||
import { Button, Form } from 'antd';
|
||||
import ProFromMap from '@/components/ProFromMap';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
import { getCrossCityAllianceList } from '@/services/jobfair/crossCityAlliance';
|
||||
|
||||
interface EditModalProps {
|
||||
open: boolean;
|
||||
@@ -18,22 +22,54 @@ interface EditModalProps {
|
||||
onSubmit: (values: API.PublicJobFair.JobFairForm) => Promise<void>;
|
||||
}
|
||||
|
||||
/** 逗号串 -> 数组(空值返回 [''] 保证至少一行) */
|
||||
const toArr = (s?: string): string[] => {
|
||||
if (!s) return [''];
|
||||
const arr = s.split(',').map((x) => x.trim()).filter(Boolean);
|
||||
return arr.length ? arr : [''];
|
||||
};
|
||||
|
||||
/** 数组 -> 逗号串(过滤空串) */
|
||||
const toStr = (arr?: string[]): string =>
|
||||
(arr || []).map((x) => (x || '').trim()).filter(Boolean).join(',');
|
||||
|
||||
const EditModal: React.FC<EditModalProps> = ({ open, values, jobFairTypeEnum, onCancel, onSubmit }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [mapOpen, setMapOpen] = useState(false);
|
||||
const [locationInfo, setLocationInfo] = useState<{ address?: string; latitude?: number; longitude?: number }>({});
|
||||
const [yesNoEnum, setYesNoEnum] = useState<any>({});
|
||||
|
||||
const isCrossDomain = Form.useWatch('isCrossDomain', form);
|
||||
|
||||
useEffect(() => {
|
||||
getDictValueEnum('sys_yes_no').then(setYesNoEnum);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.resetFields();
|
||||
if (values) {
|
||||
form.setFieldsValue(values);
|
||||
form.setFieldsValue({
|
||||
...values,
|
||||
jobFairHostUnitList: toArr(values.jobFairHostUnit),
|
||||
jobFairHelpUnitList: toArr(values.jobFairHelpUnit),
|
||||
jobFairOrganizeUnitList: toArr(values.jobFairOrganizeUnit),
|
||||
crossDomainCityList: values.crossDomainCities
|
||||
? values.crossDomainCities.split(',').map((x) => x.trim()).filter(Boolean)
|
||||
: [],
|
||||
});
|
||||
setLocationInfo({
|
||||
address: values.jobFairAddress,
|
||||
latitude: values.latitude,
|
||||
longitude: values.longitude,
|
||||
});
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
jobFairHostUnitList: [''],
|
||||
jobFairHelpUnitList: [''],
|
||||
jobFairOrganizeUnitList: [''],
|
||||
crossDomainCityList: [],
|
||||
});
|
||||
setLocationInfo({});
|
||||
}
|
||||
}
|
||||
@@ -59,8 +95,26 @@ const EditModal: React.FC<EditModalProps> = ({ open, values, jobFairTypeEnum, on
|
||||
open={open}
|
||||
width={700}
|
||||
modalProps={{ destroyOnClose: true, onCancel }}
|
||||
initialValues={{ isCrossDomain: 'n' }}
|
||||
onFinish={async (formValues) => {
|
||||
await onSubmit({ ...formValues, jobFairId: values?.jobFairId } as API.PublicJobFair.JobFairForm);
|
||||
const v = formValues as any;
|
||||
const {
|
||||
jobFairHostUnitList,
|
||||
jobFairHelpUnitList,
|
||||
jobFairOrganizeUnitList,
|
||||
crossDomainCityList,
|
||||
...rest
|
||||
} = v;
|
||||
const payload: API.PublicJobFair.JobFairForm = {
|
||||
...rest,
|
||||
jobFairId: values?.jobFairId,
|
||||
jobFairHostUnit: toStr(jobFairHostUnitList),
|
||||
jobFairHelpUnit: toStr(jobFairHelpUnitList),
|
||||
jobFairOrganizeUnit: toStr(jobFairOrganizeUnitList),
|
||||
crossDomainCities:
|
||||
isCrossDomain === 'y' ? toStr(crossDomainCityList) : '',
|
||||
};
|
||||
await onSubmit(payload);
|
||||
}}
|
||||
>
|
||||
<ProFormText name="jobFairId" hidden />
|
||||
@@ -82,6 +136,48 @@ const EditModal: React.FC<EditModalProps> = ({ open, values, jobFairTypeEnum, on
|
||||
valueEnum={jobFairTypeEnum}
|
||||
rules={[{ required: true, message: '请选择招聘会类型' }]}
|
||||
/>
|
||||
<ProFormRadio.Group
|
||||
name="isCrossDomain"
|
||||
label="是否跨域"
|
||||
valueEnum={yesNoEnum}
|
||||
/>
|
||||
{isCrossDomain === 'y' && (
|
||||
<ProFormSelect
|
||||
name="crossDomainCityList"
|
||||
label="跨域联盟城市"
|
||||
mode="multiple"
|
||||
placeholder="请选择跨域联盟城市"
|
||||
rules={[{ required: true, message: '请选择跨域联盟城市' }]}
|
||||
request={async () => {
|
||||
const res = await getCrossCityAllianceList({ pageSize: 1000 });
|
||||
return (res.rows || []).map((c) => ({ label: c.name, value: c.name }));
|
||||
}}
|
||||
fieldProps={{
|
||||
tagRender: (props: any) => {
|
||||
const { label, closable, onClose } = props;
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
margin: '0 4px 4px 0',
|
||||
padding: '2px 8px',
|
||||
background: '#f0f5ff',
|
||||
border: '1px solid #adc6ff',
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
{closable && (
|
||||
<span style={{ marginLeft: 4, cursor: 'pointer' }} onClick={onClose}>
|
||||
×
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ProForm.Group>
|
||||
<ProFormDateTimePicker
|
||||
name="jobFairStartTime"
|
||||
@@ -100,14 +196,34 @@ const EditModal: React.FC<EditModalProps> = ({ open, values, jobFairTypeEnum, on
|
||||
<ProFormDateTimePicker name="jobFairSignUpStartTime" label="报名开始时间" width="md" />
|
||||
<ProFormDateTimePicker name="jobFairSignUpEndTime" label="报名结束时间" width="md" />
|
||||
</ProForm.Group>
|
||||
<ProForm.Group>
|
||||
<ProFormText name="jobFairHostUnit" label="主办单位" width="md" placeholder="请输入主办单位" />
|
||||
<ProFormText name="jobFairCoOrganizer" label="协办单位" width="md" placeholder="请输入协办单位" />
|
||||
</ProForm.Group>
|
||||
<ProForm.Group>
|
||||
<ProFormText name="jobFairOrganizer" label="承办单位" width="md" placeholder="请输入承办单位" />
|
||||
<ProFormText name="jobFairPhone" label="联系电话" width="md" placeholder="请输入联系电话" />
|
||||
</ProForm.Group>
|
||||
<ProFormList
|
||||
name="jobFairHostUnitList"
|
||||
label="主办单位"
|
||||
min={1}
|
||||
copyIconProps={false}
|
||||
creatorButtonProps={{ position: 'bottom', creatorButtonText: '添加主办单位' }}
|
||||
>
|
||||
<ProFormText placeholder="请输入主办单位" />
|
||||
</ProFormList>
|
||||
<ProFormList
|
||||
name="jobFairHelpUnitList"
|
||||
label="协办单位"
|
||||
min={1}
|
||||
copyIconProps={false}
|
||||
creatorButtonProps={{ position: 'bottom', creatorButtonText: '添加协办单位' }}
|
||||
>
|
||||
<ProFormText placeholder="请输入协办单位" />
|
||||
</ProFormList>
|
||||
<ProFormList
|
||||
name="jobFairOrganizeUnitList"
|
||||
label="承办单位"
|
||||
min={1}
|
||||
copyIconProps={false}
|
||||
creatorButtonProps={{ position: 'bottom', creatorButtonText: '添加承办单位' }}
|
||||
>
|
||||
<ProFormText placeholder="请输入承办单位" />
|
||||
</ProFormList>
|
||||
<ProFormText name="jobFairPhone" label="联系电话" placeholder="请输入联系电话" />
|
||||
<ProFormTextArea name="jobFairIntroduction" label="招聘会简介" placeholder="请输入招聘会简介" />
|
||||
|
||||
<ProForm.Item label="地图选点">
|
||||
|
||||
@@ -22,10 +22,12 @@ const PublicJobFairList: React.FC = () => {
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [currentRow, setCurrentRow] = useState<API.PublicJobFair.JobFairItem>();
|
||||
const [jobFairTypeEnum, setJobFairTypeEnum] = useState<any>({});
|
||||
const [yesNoEnum, setYesNoEnum] = useState<any>({});
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
getDictValueEnum('job_fair_type', true).then(setJobFairTypeEnum);
|
||||
getDictValueEnum('sys_yes_no').then(setYesNoEnum);
|
||||
}, []);
|
||||
|
||||
const handleDelete = async (ids: string) => {
|
||||
@@ -100,6 +102,13 @@ const PublicJobFairList: React.FC = () => {
|
||||
valueEnum: jobFairTypeEnum,
|
||||
render: (_, record) => <DictTag enums={jobFairTypeEnum} value={record.jobFairType} />,
|
||||
},
|
||||
{
|
||||
title: '是否跨域',
|
||||
dataIndex: 'isCrossDomain',
|
||||
valueType: 'select',
|
||||
valueEnum: yesNoEnum,
|
||||
render: (_, record) => <DictTag enums={yesNoEnum} value={record.isCrossDomain} />,
|
||||
},
|
||||
{
|
||||
title: '开始时间',
|
||||
dataIndex: 'jobFairStartTime',
|
||||
|
||||
Reference in New Issue
Block a user