feat: implement job management for companies in public job fairs
Some checks failed
Node CI / build (14.x, macOS-latest) (push) Has been cancelled
Node CI / build (14.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (14.x, windows-latest) (push) Has been cancelled
Node CI / build (16.x, macOS-latest) (push) Has been cancelled
Node CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (16.x, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
coverage CI / build (push) Has been cancelled
Node pnpm CI / build (16.x, macOS-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, windows-latest) (push) Has been cancelled
Some checks failed
Node CI / build (14.x, macOS-latest) (push) Has been cancelled
Node CI / build (14.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (14.x, windows-latest) (push) Has been cancelled
Node CI / build (16.x, macOS-latest) (push) Has been cancelled
Node CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (16.x, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
coverage CI / build (push) Has been cancelled
Node pnpm CI / build (16.x, macOS-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, windows-latest) (push) Has been cancelled
This commit is contained in:
@@ -243,6 +243,7 @@ export default [
|
||||
name: '招聘会详情',
|
||||
path: '/jobfair/public-job-fair/detail',
|
||||
component: './Jobfair/PublicJobFair/Detail',
|
||||
access: 'canViewPublicJobFairDetail',
|
||||
},
|
||||
{
|
||||
name: '报名人员',
|
||||
|
||||
@@ -4,7 +4,7 @@ set -Eeuo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/shihezi}"
|
||||
DEPLOY_HOST="${DEPLOY_HOST:-39.98.44.136}"
|
||||
DEPLOY_HOST="${DEPLOY_HOST:-47.111.103.66}"
|
||||
DEPLOY_USER="${DEPLOY_USER:-root}"
|
||||
DEPLOY_SSH_PORT="${DEPLOY_SSH_PORT:-22}"
|
||||
DEPLOY_PATH="${DEPLOY_PATH:-/opt/service/project/shz-admin/dist}"
|
||||
@@ -40,7 +40,7 @@ usage() {
|
||||
默认配置(均可通过环境变量覆盖):
|
||||
DEPLOY_USER SSH 用户名,默认 root
|
||||
DEPLOY_PATH 服务器上的前端静态文件目录,默认 /opt/service/project/shz-admin/dist
|
||||
DEPLOY_HOST SSH 主机,默认 39.98.44.136
|
||||
DEPLOY_HOST SSH 主机,默认 47.111.103.66
|
||||
DEPLOY_SSH_PORT SSH 端口,默认 22
|
||||
BUILD_DIR 本地构建目录,默认 ./shihezi(由 config/config.ts 的 outputPath 决定)
|
||||
YARN_CMD Yarn 命令,默认 yarn
|
||||
|
||||
44
scripts/nginx-shz-admin.conf
Normal file
44
scripts/nginx-shz-admin.conf
Normal file
@@ -0,0 +1,44 @@
|
||||
# shz-admin 前端与现有 SHZ 后端代理配置。
|
||||
# 安装位置:/etc/nginx/default.d/shz-admin.conf
|
||||
|
||||
client_max_body_size 100m;
|
||||
|
||||
location = /shihezi {
|
||||
return 301 /shihezi/;
|
||||
}
|
||||
|
||||
location = /shihezi/index.html {
|
||||
alias /opt/service/project/shz-admin/dist/index.html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
||||
}
|
||||
|
||||
location /shihezi/ {
|
||||
alias /opt/service/project/shz-admin/dist/;
|
||||
try_files $uri $uri/ /shihezi/index.html;
|
||||
add_header Cache-Control "no-cache" always;
|
||||
}
|
||||
|
||||
location /api/shihezi/ {
|
||||
# 对外保留 /api/shihezi/ 前缀,转发到本机后端时剥离该前缀。
|
||||
proxy_pass http://127.0.0.1:9091/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $proxy_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
location /data/file/ {
|
||||
proxy_pass http://127.0.0.1:9091/data/file/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $proxy_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
@@ -5,6 +5,14 @@ import { clearTokenCache } from './utils/tokenCache';
|
||||
* */
|
||||
export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) {
|
||||
const { currentUser } = initialState ?? {};
|
||||
const roles = Array.isArray(currentUser?.roles) ? (currentUser.roles as unknown[]) : [];
|
||||
const isEnterprise =
|
||||
currentUser?.userType === 'view' ||
|
||||
roles.some((role) => {
|
||||
if (typeof role === 'string') return role === 'view';
|
||||
if (!role || typeof role !== 'object') return false;
|
||||
return (role as { roleKey?: string }).roleKey === 'view';
|
||||
});
|
||||
const hasPerms = (perm: string) => {
|
||||
return matchPermission(initialState?.currentUser?.permissions, perm);
|
||||
};
|
||||
@@ -13,6 +21,8 @@ export default function access(initialState: { currentUser?: API.CurrentUser } |
|
||||
};
|
||||
return {
|
||||
canAdmin: currentUser && currentUser.access === 'admin',
|
||||
isEnterprise,
|
||||
canViewPublicJobFairDetail: !isEnterprise && hasPerms('cms:publicJobFair:query'),
|
||||
hasPerms,
|
||||
roleFiler,
|
||||
};
|
||||
|
||||
@@ -1,25 +1,58 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { history, useSearchParams } from '@umijs/max';
|
||||
import { Button, Card, Collapse, Descriptions, message, Modal, Space, Tag, List } from 'antd';
|
||||
import { history, useAccess, useSearchParams } from '@umijs/max';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Collapse,
|
||||
Descriptions,
|
||||
Form,
|
||||
Input,
|
||||
List,
|
||||
message,
|
||||
Modal,
|
||||
Radio,
|
||||
Result,
|
||||
Space,
|
||||
Tag,
|
||||
} from 'antd';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { ArrowLeftOutlined, PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { ArrowLeftOutlined, AuditOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
getPublicJobFairDetail,
|
||||
removeCompanyFromJobFair,
|
||||
removeJobFromJobFair,
|
||||
reviewPublicJobFairCompany,
|
||||
} from '@/services/jobfair/publicJobFair';
|
||||
import CompanySelectModal from '../components/CompanySelectModal';
|
||||
import JobSelectModal from '../components/JobSelectModal';
|
||||
|
||||
type ReviewFormValues = {
|
||||
reviewStatus: '1' | '2';
|
||||
reviewRemark?: string;
|
||||
};
|
||||
|
||||
const reviewStatusMap: Record<string, { text: string; color: string }> = {
|
||||
'0': { text: '待审核', color: 'gold' },
|
||||
'1': { text: '已通过', color: 'green' },
|
||||
'2': { text: '已驳回', color: 'red' },
|
||||
};
|
||||
|
||||
const JobFairDetail: React.FC = () => {
|
||||
const access = useAccess();
|
||||
const isEnterprise = Boolean(access.isEnterprise);
|
||||
const [searchParams] = useSearchParams();
|
||||
const jobFairId = searchParams.get('id') || '';
|
||||
|
||||
const [detail, setDetail] = useState<API.PublicJobFair.JobFairItem & { companyList?: API.PublicJobFair.CompanyInfo[] }>();
|
||||
const [detail, setDetail] = useState<API.PublicJobFair.DetailResult['data']>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [companyModalOpen, setCompanyModalOpen] = useState(false);
|
||||
const [jobModalOpen, setJobModalOpen] = useState(false);
|
||||
const [currentCompany, setCurrentCompany] = useState<API.PublicJobFair.CompanyInfo>();
|
||||
const [reviewModalOpen, setReviewModalOpen] = useState(false);
|
||||
const [reviewCompany, setReviewCompany] = useState<API.PublicJobFair.CompanyInfo>();
|
||||
const [reviewSubmitting, setReviewSubmitting] = useState(false);
|
||||
const [reviewForm] = Form.useForm<ReviewFormValues>();
|
||||
const reviewStatus = Form.useWatch('reviewStatus', reviewForm);
|
||||
|
||||
const fetchDetail = async () => {
|
||||
if (!jobFairId) return;
|
||||
@@ -28,19 +61,23 @@ const JobFairDetail: React.FC = () => {
|
||||
setLoading(false);
|
||||
if (res.code === 200) {
|
||||
setDetail(res.data);
|
||||
} else {
|
||||
message.error(res.msg || '招聘会详情获取失败');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEnterprise) {
|
||||
fetchDetail();
|
||||
}, [jobFairId]);
|
||||
}
|
||||
}, [jobFairId, isEnterprise]);
|
||||
|
||||
const handleRemoveCompany = (id: string) => {
|
||||
const handleRemoveCompany = (company: API.PublicJobFair.CompanyInfo) => {
|
||||
Modal.confirm({
|
||||
title: '确认移除',
|
||||
content: '确定要移除该企业吗?',
|
||||
content: `确定移除「${company.companyName}」吗?审核未通过的企业不能移除。`,
|
||||
onOk: async () => {
|
||||
const res = await removeCompanyFromJobFair(id);
|
||||
const res = await removeCompanyFromJobFair(company.id);
|
||||
if (res.code === 200) {
|
||||
message.success('移除成功');
|
||||
fetchDetail();
|
||||
@@ -51,24 +88,64 @@ const JobFairDetail: React.FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveJob = (id: string) => {
|
||||
const handleRemoveJob = (job: API.PublicJobFair.JobInfo) => {
|
||||
Modal.confirm({
|
||||
title: '确认移除',
|
||||
content: '确定要移除该岗位吗?',
|
||||
title: '确认移除岗位',
|
||||
content: `确定移除「${job.jobTitle}」吗?`,
|
||||
onOk: async () => {
|
||||
const res = await removeJobFromJobFair(id);
|
||||
const res = job.fairRelationId
|
||||
? await removeJobFromJobFair(job.fairRelationId)
|
||||
: { code: 500, msg: '未找到招聘会岗位关联' };
|
||||
if (res.code === 200) {
|
||||
message.success('移除成功');
|
||||
message.success('岗位已移除');
|
||||
fetchDetail();
|
||||
} else {
|
||||
message.error(res.msg || '移除失败');
|
||||
message.error(res.msg || '岗位移除失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const openReviewModal = (company: API.PublicJobFair.CompanyInfo) => {
|
||||
setReviewCompany(company);
|
||||
reviewForm.setFieldsValue({ reviewStatus: '1', reviewRemark: '' });
|
||||
setReviewModalOpen(true);
|
||||
};
|
||||
|
||||
const submitReview = async (values: ReviewFormValues) => {
|
||||
if (!reviewCompany) return;
|
||||
setReviewSubmitting(true);
|
||||
const res = await reviewPublicJobFairCompany(jobFairId, reviewCompany.id, values);
|
||||
setReviewSubmitting(false);
|
||||
if (res.code === 200) {
|
||||
message.success('审核完成');
|
||||
setReviewModalOpen(false);
|
||||
setReviewCompany(undefined);
|
||||
fetchDetail();
|
||||
} else {
|
||||
message.error(res.msg || '审核失败');
|
||||
}
|
||||
};
|
||||
|
||||
const jobFairTypeMap: Record<string, string> = { '1': '线上', '2': '线下' };
|
||||
|
||||
if (isEnterprise) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<Result
|
||||
status="403"
|
||||
title="企业用户不能查看招聘会详情"
|
||||
subTitle="请在招聘会列表完成报名,并在审核通过后使用“编辑报名岗位”。"
|
||||
extra={
|
||||
<Button type="primary" onClick={() => history.replace('/jobfair/public-job-fair')}>
|
||||
返回招聘会列表
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
loading={loading}
|
||||
@@ -81,11 +158,15 @@ const JobFairDetail: React.FC = () => {
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Card title="基本信息" style={{ marginBottom: 16 }} extra={
|
||||
<Card
|
||||
title="基本信息"
|
||||
style={{ marginBottom: 16 }}
|
||||
extra={
|
||||
<Button onClick={() => history.push(`/jobfair/public-job-fair/signups?id=${jobFairId}`)}>
|
||||
查看全部报名
|
||||
</Button>
|
||||
}>
|
||||
}
|
||||
>
|
||||
<Descriptions column={2}>
|
||||
<Descriptions.Item label="标题">{detail?.jobFairTitle}</Descriptions.Item>
|
||||
<Descriptions.Item label="类型">
|
||||
@@ -107,32 +188,67 @@ const JobFairDetail: React.FC = () => {
|
||||
<Descriptions.Item label="跨域联盟城市" span={2}>
|
||||
{detail?.crossDomainCities || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="简介" span={2}>{detail?.jobFairIntroduction}</Descriptions.Item>
|
||||
<Descriptions.Item label="简介" span={2}>
|
||||
{detail?.jobFairIntroduction}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="参会企业及岗位"
|
||||
extra={
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCompanyModalOpen(true)}>
|
||||
access.hasPerms('cms:publicJobFair:company:add') ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setCompanyModalOpen(true)}
|
||||
>
|
||||
添加企业
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{detail?.companyList?.length ? (
|
||||
<Collapse>
|
||||
{detail.companyList.map((company) => (
|
||||
{detail.companyList.map((company) => {
|
||||
const reviewStatusValue = company.reviewStatus || '0';
|
||||
const reviewStatus = reviewStatusMap[reviewStatusValue] || reviewStatusMap['0'];
|
||||
const isApproved = reviewStatusValue === '1';
|
||||
const canReview = access.hasPerms('cms:publicJobFair:review');
|
||||
const canAdminAddJob = Boolean(
|
||||
isApproved && access.hasPerms('cms:publicJobFair:job:add'),
|
||||
);
|
||||
const canRemoveCompany = Boolean(
|
||||
isApproved && access.hasPerms('cms:publicJobFair:company:remove'),
|
||||
);
|
||||
const canRemoveJob =
|
||||
isApproved &&
|
||||
access.hasPerms('cms:publicJobFair:job:remove') &&
|
||||
(company.jobInfoList || []).length > 0;
|
||||
|
||||
return (
|
||||
<Collapse.Panel
|
||||
key={company.id}
|
||||
header={
|
||||
<Space>
|
||||
<Space wrap>
|
||||
<span style={{ fontWeight: 500 }}>{company.companyName}</span>
|
||||
<Tag>{company.industry}</Tag>
|
||||
<Tag>{company.scale}</Tag>
|
||||
<Tag color={reviewStatus.color}>{reviewStatus.text}</Tag>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space onClick={(e) => e.stopPropagation()}>
|
||||
{canReview && (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<AuditOutlined />}
|
||||
onClick={() => openReviewModal(company)}
|
||||
>
|
||||
审核
|
||||
</Button>
|
||||
)}
|
||||
{canAdminAddJob && (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
@@ -143,46 +259,64 @@ const JobFairDetail: React.FC = () => {
|
||||
>
|
||||
添加岗位
|
||||
</Button>
|
||||
)}
|
||||
{canRemoveCompany && (
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleRemoveCompany(company.id)}
|
||||
onClick={() => handleRemoveCompany(company)}
|
||||
>
|
||||
移除企业
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{company.reviewStatus === '2' && company.reviewRemark && (
|
||||
<div style={{ marginBottom: 12, color: '#cf1322' }}>
|
||||
驳回原因:{company.reviewRemark}
|
||||
</div>
|
||||
)}
|
||||
<List
|
||||
size="small"
|
||||
dataSource={company.jobInfoList || []}
|
||||
renderItem={(job) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
...(canRemoveJob
|
||||
? [
|
||||
<Button
|
||||
key="remove"
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
onClick={() => handleRemoveJob(job.id!)}
|
||||
onClick={() => handleRemoveJob(job)}
|
||||
>
|
||||
移除
|
||||
</Button>,
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
>
|
||||
<Space>
|
||||
<Space wrap>
|
||||
<span>{job.jobTitle}</span>
|
||||
<Tag color="orange">{job.minSalary}K-{job.maxSalary}K</Tag>
|
||||
<Tag>{job.education}</Tag>
|
||||
<Tag>招{job.vacancies}人</Tag>
|
||||
<Tag color="orange">
|
||||
{job.minSalary || job.maxSalary
|
||||
? `${job.minSalary || 0}-${job.maxSalary || 0} 元/月`
|
||||
: '面议'}
|
||||
</Tag>
|
||||
{job.education && <Tag>{job.education}</Tag>}
|
||||
{job.experience && <Tag>{job.experience}</Tag>}
|
||||
{job.vacancies !== undefined && <Tag>招{job.vacancies}人</Tag>}
|
||||
</Space>
|
||||
</List.Item>
|
||||
)}
|
||||
locale={{ emptyText: '暂无岗位' }}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</Collapse>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: 24, color: '#999' }}>暂无参会企业</div>
|
||||
@@ -213,6 +347,34 @@ const JobFairDetail: React.FC = () => {
|
||||
fetchDetail();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={`审核参会企业${reviewCompany ? ` - ${reviewCompany.companyName}` : ''}`}
|
||||
open={reviewModalOpen}
|
||||
confirmLoading={reviewSubmitting}
|
||||
onCancel={() => {
|
||||
setReviewModalOpen(false);
|
||||
setReviewCompany(undefined);
|
||||
}}
|
||||
onOk={() => reviewForm.submit()}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={reviewForm} layout="vertical" onFinish={submitReview}>
|
||||
<Form.Item name="reviewStatus" label="审核结果" rules={[{ required: true }]}>
|
||||
<Radio.Group>
|
||||
<Radio value="1">通过</Radio>
|
||||
<Radio value="2">驳回</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="reviewRemark"
|
||||
label="审核意见"
|
||||
rules={reviewStatus === '2' ? [{ required: true, message: '驳回时请填写原因' }] : []}
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="驳回时请输入原因" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Button, List, message, Modal, Space, Tag } from 'antd';
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
addCurrentCompanyJob,
|
||||
getCurrentCompanyJobs,
|
||||
removeCurrentCompanyJob,
|
||||
updateCurrentCompanyJob,
|
||||
type PublicJobFairJobForm,
|
||||
} from '@/services/jobfair/publicJobFair';
|
||||
import JobFormModal from './JobFormModal';
|
||||
|
||||
interface CompanyJobManageModalProps {
|
||||
open: boolean;
|
||||
jobFairId: string;
|
||||
jobFairTitle?: string;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
/** 企业报名通过后,直接在列表弹窗维护自己的报名岗位。 */
|
||||
const CompanyJobManageModal: React.FC<CompanyJobManageModalProps> = ({
|
||||
open,
|
||||
jobFairId,
|
||||
jobFairTitle,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [jobs, setJobs] = useState<API.PublicJobFair.JobInfo[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [jobFormOpen, setJobFormOpen] = useState(false);
|
||||
const [editingJob, setEditingJob] = useState<API.PublicJobFair.JobInfo>();
|
||||
|
||||
const loadJobs = useCallback(async () => {
|
||||
if (!jobFairId) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getCurrentCompanyJobs(jobFairId);
|
||||
if (res.code === 200) {
|
||||
setJobs(res.data || []);
|
||||
} else {
|
||||
message.error(res.msg || '报名岗位获取失败');
|
||||
}
|
||||
} catch {
|
||||
message.error('报名岗位获取失败,请稍后重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [jobFairId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setEditingJob(undefined);
|
||||
void loadJobs();
|
||||
}, [open, loadJobs]);
|
||||
|
||||
const closeJobForm = () => {
|
||||
setJobFormOpen(false);
|
||||
setEditingJob(undefined);
|
||||
};
|
||||
|
||||
const submitJob = async (values: PublicJobFairJobForm) => {
|
||||
if (editingJob) {
|
||||
return updateCurrentCompanyJob(jobFairId, editingJob.jobId, values);
|
||||
}
|
||||
return addCurrentCompanyJob(jobFairId, values);
|
||||
};
|
||||
|
||||
const removeJob = (job: API.PublicJobFair.JobInfo) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除报名岗位',
|
||||
content: `确定删除「${job.jobTitle}」吗?删除后需要重新新增才能报名该岗位。`,
|
||||
onOk: async () => {
|
||||
const res = await removeCurrentCompanyJob(jobFairId, job.jobId);
|
||||
if (res.code === 200) {
|
||||
message.success('报名岗位已删除');
|
||||
loadJobs();
|
||||
} else {
|
||||
message.error(res.msg || '报名岗位删除失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title={`编辑报名岗位${jobFairTitle ? ` - ${jobFairTitle}` : ''}`}
|
||||
open={open}
|
||||
width={800}
|
||||
footer={null}
|
||||
onCancel={() => {
|
||||
closeJobForm();
|
||||
onCancel();
|
||||
}}
|
||||
destroyOnClose
|
||||
>
|
||||
<div style={{ marginBottom: 16, textAlign: 'right' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
setEditingJob(undefined);
|
||||
setJobFormOpen(true);
|
||||
}}
|
||||
>
|
||||
新增报名岗位
|
||||
</Button>
|
||||
</div>
|
||||
<List
|
||||
loading={loading}
|
||||
bordered
|
||||
dataSource={jobs}
|
||||
locale={{ emptyText: '暂无报名岗位,请先新增岗位' }}
|
||||
renderItem={(job) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button
|
||||
key="edit"
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => {
|
||||
setEditingJob(job);
|
||||
setJobFormOpen(true);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>,
|
||||
<Button
|
||||
key="remove"
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => removeJob(job)}
|
||||
>
|
||||
删除
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Space wrap>
|
||||
<span>{job.jobTitle}</span>
|
||||
<Tag color="orange">
|
||||
{job.minSalary || job.maxSalary
|
||||
? `${job.minSalary || 0}-${job.maxSalary || 0} 元/月`
|
||||
: '面议'}
|
||||
</Tag>
|
||||
{job.education && <Tag>{job.education}</Tag>}
|
||||
{job.experience && <Tag>{job.experience}</Tag>}
|
||||
{job.vacancies !== undefined && <Tag>招{job.vacancies}人</Tag>}
|
||||
</Space>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<JobFormModal
|
||||
open={jobFormOpen}
|
||||
job={editingJob}
|
||||
onCancel={closeJobForm}
|
||||
onSubmit={submitJob}
|
||||
onSuccess={() => {
|
||||
closeJobForm();
|
||||
loadJobs();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompanyJobManageModal;
|
||||
79
src/pages/Jobfair/PublicJobFair/components/JobFormModal.tsx
Normal file
79
src/pages/Jobfair/PublicJobFair/components/JobFormModal.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { ModalForm, ProFormDigit, ProFormText, ProFormTextArea } from '@ant-design/pro-components';
|
||||
import { Form, message } from 'antd';
|
||||
import type { PublicJobFairJobForm } from '@/services/jobfair/publicJobFair';
|
||||
|
||||
interface JobFormModalProps {
|
||||
open: boolean;
|
||||
job?: API.PublicJobFair.JobInfo;
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
onSubmit: (values: PublicJobFairJobForm) => Promise<API.PublicJobFair.CommonResult>;
|
||||
}
|
||||
|
||||
/** 企业报名岗位新增/编辑表单。企业 ID 由后端从登录用户解析,不接受前端传入。 */
|
||||
const JobFormModal: React.FC<JobFormModalProps> = ({
|
||||
open,
|
||||
job,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const [form] = Form.useForm<PublicJobFairJobForm>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
form.resetFields();
|
||||
if (job) {
|
||||
form.setFieldsValue({
|
||||
jobId: job.jobId,
|
||||
jobTitle: job.jobTitle,
|
||||
minSalary: job.minSalary,
|
||||
maxSalary: job.maxSalary,
|
||||
education: job.education,
|
||||
experience: job.experience,
|
||||
vacancies: job.vacancies,
|
||||
jobAddress: job.jobAddress,
|
||||
description: job.description,
|
||||
jobCategory: job.jobCategory,
|
||||
type: job.type,
|
||||
});
|
||||
}
|
||||
}, [open, job, form]);
|
||||
|
||||
return (
|
||||
<ModalForm<PublicJobFairJobForm>
|
||||
title={job ? '编辑报名岗位' : '新增报名岗位'}
|
||||
form={form}
|
||||
open={open}
|
||||
width={680}
|
||||
modalProps={{ destroyOnClose: true, onCancel }}
|
||||
onFinish={async (values) => {
|
||||
const res = await onSubmit({ ...values, jobId: job?.jobId });
|
||||
if (res.code === 200) {
|
||||
message.success(job ? '岗位修改成功' : '岗位新增成功');
|
||||
onSuccess();
|
||||
return true;
|
||||
}
|
||||
message.error(res.msg || '岗位操作失败');
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<ProFormText
|
||||
name="jobTitle"
|
||||
label="岗位名称"
|
||||
placeholder="请输入岗位名称"
|
||||
rules={[{ required: true, message: '请输入岗位名称' }]}
|
||||
/>
|
||||
<ProFormDigit name="minSalary" label="最低薪资(元/月)" min={0} />
|
||||
<ProFormDigit name="maxSalary" label="最高薪资(元/月)" min={0} />
|
||||
<ProFormText name="education" label="学历要求" placeholder="如:本科" />
|
||||
<ProFormText name="experience" label="工作经验" placeholder="如:1-3年" />
|
||||
<ProFormDigit name="vacancies" label="招聘人数" min={0} />
|
||||
<ProFormText name="jobAddress" label="工作地点" />
|
||||
<ProFormTextArea name="description" label="岗位描述" />
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
|
||||
export default JobFormModal;
|
||||
@@ -1,26 +1,37 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { useAccess, history } from '@umijs/max';
|
||||
import { Button, FormInstance, message, Modal } from 'antd';
|
||||
import { Button, FormInstance, message, Modal, Tag } from 'antd';
|
||||
import { ActionType, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import { PlusOutlined, DeleteOutlined, FormOutlined, ExportOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
FormOutlined,
|
||||
ExportOutlined,
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { getDictValueEnum } from '@/services/system/dict';
|
||||
import DictTag from '@/components/DictTag';
|
||||
import {
|
||||
getPublicJobFairList,
|
||||
getPublicJobFairDetail,
|
||||
signupPublicJobFair,
|
||||
addPublicJobFair,
|
||||
updatePublicJobFair,
|
||||
deletePublicJobFair,
|
||||
exportPublicJobFair,
|
||||
} from '@/services/jobfair/publicJobFair';
|
||||
import EditModal from './components/EditModal';
|
||||
import CompanyJobManageModal from './components/CompanyJobManageModal';
|
||||
|
||||
const PublicJobFairList: React.FC = () => {
|
||||
const access = useAccess();
|
||||
const isEnterprise = Boolean(access.isEnterprise);
|
||||
const formRef = useRef<FormInstance>();
|
||||
const actionRef = useRef<ActionType>();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [currentRow, setCurrentRow] = useState<API.PublicJobFair.JobFairItem>();
|
||||
const [companyJobModalOpen, setCompanyJobModalOpen] = useState(false);
|
||||
const [currentJobFair, setCurrentJobFair] = useState<API.PublicJobFair.JobFairItem>();
|
||||
const [jobFairTypeEnum, setJobFairTypeEnum] = useState<any>({});
|
||||
const [yesNoEnum, setYesNoEnum] = useState<any>({});
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
@@ -47,6 +58,22 @@ const PublicJobFairList: React.FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSignup = (record: API.PublicJobFair.JobFairItem) => {
|
||||
Modal.confirm({
|
||||
title: '报名线上招聘会',
|
||||
content: `确定报名「${record.jobFairTitle}」吗?报名后需要等待管理员审核。`,
|
||||
onOk: async () => {
|
||||
const res = await signupPublicJobFair(record.jobFairId);
|
||||
if (res.code === 200) {
|
||||
message.success('报名成功,请等待管理员审核');
|
||||
actionRef.current?.reload();
|
||||
} else {
|
||||
message.error(res.msg || '报名失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const buildListSearchParams = (values: Record<string, any>): API.PublicJobFair.ListParams => {
|
||||
const { dateRange, current, pageSize, ...rest } = values || {};
|
||||
const params = { ...rest } as API.PublicJobFair.ListParams;
|
||||
@@ -102,6 +129,32 @@ const PublicJobFairList: React.FC = () => {
|
||||
valueEnum: jobFairTypeEnum,
|
||||
render: (_, record) => <DictTag enums={jobFairTypeEnum} value={record.jobFairType} />,
|
||||
},
|
||||
{
|
||||
title: '报名审核状态',
|
||||
dataIndex: 'enterpriseReviewStatus',
|
||||
hideInSearch: true,
|
||||
hideInTable: !isEnterprise,
|
||||
render: (_, record) => {
|
||||
if (record.isSignUp === undefined) return '-';
|
||||
if (record.isSignUp !== '1') return <Tag>未报名</Tag>;
|
||||
const statusMap: Record<string, { text: string; color: string }> = {
|
||||
'0': { text: '待审核', color: 'gold' },
|
||||
'1': { text: '已通过', color: 'green' },
|
||||
'2': { text: '已驳回', color: 'red' },
|
||||
};
|
||||
const status = statusMap[record.enterpriseReviewStatus || '0'] || statusMap['0'];
|
||||
return (
|
||||
<span>
|
||||
<Tag color={status.color}>{status.text}</Tag>
|
||||
{record.enterpriseReviewComments && (
|
||||
<span title={record.enterpriseReviewComments} style={{ color: '#999' }}>
|
||||
(有意见)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '是否跨域',
|
||||
dataIndex: 'isCrossDomain',
|
||||
@@ -130,9 +183,21 @@ const PublicJobFairList: React.FC = () => {
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
width: 200,
|
||||
width: 360,
|
||||
fixed: 'right',
|
||||
render: (_, record) => [
|
||||
render: (_, record) => {
|
||||
const canViewDetail = !isEnterprise && access.hasPerms('cms:publicJobFair:query');
|
||||
const canSignUp =
|
||||
isEnterprise &&
|
||||
access.hasPerms('cms:publicJobFair:signup') &&
|
||||
(record.canSignUp ?? (record.isSignUp !== '1' || record.enterpriseReviewStatus === '2'));
|
||||
const canEditCompanyJobs =
|
||||
isEnterprise &&
|
||||
access.hasPerms('cms:publicJobFair:job:edit') &&
|
||||
(record.canEditJobs ?? record.enterpriseReviewStatus === '1');
|
||||
|
||||
return [
|
||||
canViewDetail && (
|
||||
<Button
|
||||
key="detail"
|
||||
type="link"
|
||||
@@ -141,7 +206,33 @@ const PublicJobFairList: React.FC = () => {
|
||||
onClick={() => history.push(`/jobfair/public-job-fair/detail?id=${record.jobFairId}`)}
|
||||
>
|
||||
详情
|
||||
</Button>,
|
||||
</Button>
|
||||
),
|
||||
canSignUp && (
|
||||
<Button
|
||||
key="signup"
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => handleSignup(record)}
|
||||
>
|
||||
报名
|
||||
</Button>
|
||||
),
|
||||
canEditCompanyJobs && (
|
||||
<Button
|
||||
key="companyJobs"
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<FormOutlined />}
|
||||
onClick={() => {
|
||||
setCurrentJobFair(record);
|
||||
setCompanyJobModalOpen(true);
|
||||
}}
|
||||
>
|
||||
编辑报名岗位
|
||||
</Button>
|
||||
),
|
||||
<Button
|
||||
key="edit"
|
||||
type="link"
|
||||
@@ -169,7 +260,8 @@ const PublicJobFairList: React.FC = () => {
|
||||
>
|
||||
删除
|
||||
</Button>,
|
||||
],
|
||||
];
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -248,6 +340,15 @@ const PublicJobFairList: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<CompanyJobManageModal
|
||||
open={companyJobModalOpen}
|
||||
jobFairId={currentJobFair?.jobFairId || ''}
|
||||
jobFairTitle={currentJobFair?.jobFairTitle}
|
||||
onCancel={() => {
|
||||
setCompanyJobModalOpen(false);
|
||||
setCurrentJobFair(undefined);
|
||||
}}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -112,10 +112,11 @@ export default function ThirdPartyRedirect() {
|
||||
console.log('userType value:', userInfo?.userType, 'type:', typeof userInfo?.userType);
|
||||
if (menus && menus.length > 0 && userInfo?.userType !== 'common') {
|
||||
setRemoteMenu(menus);
|
||||
window.location.replace('http://39.98.44.136:6024/shihezi/management/management/list/index?type=1')
|
||||
window.location.replace(
|
||||
`${window.location.origin}/shihezi/management/management/list/index?type=1`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
message.error('暂时无法加载您的信息');
|
||||
setIsError(true);
|
||||
|
||||
@@ -3,6 +3,28 @@ import { downLoadXlsx } from '@/utils/downloadfile';
|
||||
|
||||
const BASE_URL = '/api/cms/publicJobFair';
|
||||
|
||||
/** 企业报名审核请求 */
|
||||
export interface PublicJobFairCompanyReviewParams {
|
||||
reviewStatus: '0' | '1' | '2';
|
||||
reviewRemark?: string;
|
||||
}
|
||||
|
||||
/** 企业报名岗位表单 */
|
||||
export interface PublicJobFairJobForm {
|
||||
jobId?: number;
|
||||
jobTitle: string;
|
||||
minSalary?: number;
|
||||
maxSalary?: number;
|
||||
education?: string;
|
||||
experience?: string;
|
||||
vacancies?: number;
|
||||
jobLocation?: string;
|
||||
jobAddress?: string;
|
||||
description?: string;
|
||||
jobCategory?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
/** 跨域招聘会岗位列表项 */
|
||||
export interface CrossDomainJobItem {
|
||||
jobId: number;
|
||||
@@ -61,6 +83,28 @@ export async function getPublicJobFairDetail(jobFairId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 企业报名招聘会 */
|
||||
export async function signupPublicJobFair(jobFairId: string) {
|
||||
return request<API.PublicJobFair.CommonResult>(`${BASE_URL}/${jobFairId}/company/signup`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
/** 管理员审核企业报名 */
|
||||
export async function reviewPublicJobFairCompany(
|
||||
jobFairId: string,
|
||||
relationId: string,
|
||||
data: PublicJobFairCompanyReviewParams,
|
||||
) {
|
||||
return request<API.PublicJobFair.CommonResult>(
|
||||
`${BASE_URL}/${jobFairId}/companies/${relationId}/review`,
|
||||
{
|
||||
method: 'PUT',
|
||||
data,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增招聘会 */
|
||||
export async function addPublicJobFair(data: API.PublicJobFair.JobFairForm) {
|
||||
return request<API.PublicJobFair.CommonResult>(BASE_URL, {
|
||||
@@ -92,7 +136,6 @@ export async function addCompanyToJobFair(data: API.PublicJobFair.AddCompanyPara
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/** 批量添加企业到招聘会 */
|
||||
export async function batchAddCompanyToJobFair(data: API.PublicJobFair.AddCompanyParams[]) {
|
||||
return request<API.PublicJobFair.CommonResult>(`${BASE_URL}/company/batch`, {
|
||||
@@ -131,8 +174,45 @@ export async function removeJobFromJobFair(id: string) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 企业新增报名岗位 */
|
||||
export async function addCurrentCompanyJob(jobFairId: string, data: PublicJobFairJobForm) {
|
||||
return request<API.PublicJobFair.CommonResult>(`${BASE_URL}/${jobFairId}/company/jobs`, {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 企业修改报名岗位 */
|
||||
export async function updateCurrentCompanyJob(
|
||||
jobFairId: string,
|
||||
jobId: number,
|
||||
data: PublicJobFairJobForm,
|
||||
) {
|
||||
return request<API.PublicJobFair.CommonResult>(`${BASE_URL}/${jobFairId}/company/jobs/${jobId}`, {
|
||||
method: 'PUT',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 企业删除报名岗位 */
|
||||
export async function removeCurrentCompanyJob(jobFairId: string, jobId: number) {
|
||||
return request<API.PublicJobFair.CommonResult>(`${BASE_URL}/${jobFairId}/company/jobs/${jobId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/** 查询当前企业在招聘会中已报名的岗位 */
|
||||
export async function getCurrentCompanyJobs(jobFairId: string) {
|
||||
return request<API.PublicJobFair.CompanyJobsResult>(`${BASE_URL}/${jobFairId}/company/jobs`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
/** 查询招聘会报名列表 */
|
||||
export async function getJobFairSignups(jobFairId: string, params?: { userName?: string; status?: string }) {
|
||||
export async function getJobFairSignups(
|
||||
jobFairId: string,
|
||||
params?: { userName?: string; status?: string },
|
||||
) {
|
||||
return request<API.PublicJobFair.SignupListResult>(`${BASE_URL}/signups/${jobFairId}`, {
|
||||
method: 'GET',
|
||||
params,
|
||||
|
||||
27
src/types/jobfair/publicJobFair.d.ts
vendored
27
src/types/jobfair/publicJobFair.d.ts
vendored
@@ -34,6 +34,12 @@ declare namespace API.PublicJobFair {
|
||||
isCrossDomain?: string;
|
||||
crossDomainCities?: string;
|
||||
createTime?: string;
|
||||
isSignUp?: string;
|
||||
canSignUp?: boolean;
|
||||
canEditJobs?: boolean;
|
||||
enterpriseId?: string;
|
||||
enterpriseReviewStatus?: '0' | '1' | '2';
|
||||
enterpriseReviewComments?: string;
|
||||
}
|
||||
|
||||
/** 招聘会列表响应 */
|
||||
@@ -47,6 +53,7 @@ declare namespace API.PublicJobFair {
|
||||
/** 岗位信息 */
|
||||
export interface JobInfo {
|
||||
id?: string;
|
||||
fairRelationId?: string;
|
||||
jobId: number;
|
||||
jobTitle: string;
|
||||
minSalary?: number;
|
||||
@@ -54,6 +61,10 @@ declare namespace API.PublicJobFair {
|
||||
education?: string;
|
||||
experience?: string;
|
||||
vacancies?: number;
|
||||
jobAddress?: string;
|
||||
description?: string;
|
||||
jobCategory?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
/** 企业信息 */
|
||||
@@ -64,10 +75,17 @@ declare namespace API.PublicJobFair {
|
||||
scale?: string;
|
||||
industry?: string;
|
||||
companyType?: string;
|
||||
reviewStatus?: '0' | '1' | '2';
|
||||
reviewBy?: string;
|
||||
reviewTime?: string;
|
||||
reviewRemark?: string;
|
||||
canReview?: boolean;
|
||||
canAddJob?: boolean;
|
||||
canRemoveCompany?: boolean;
|
||||
canEditJobs?: boolean;
|
||||
jobInfoList?: JobInfo[];
|
||||
}
|
||||
|
||||
|
||||
/** 招聘会详情响应 */
|
||||
export interface DetailResult {
|
||||
code: number;
|
||||
@@ -137,4 +155,11 @@ declare namespace API.PublicJobFair {
|
||||
msg?: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
/** 当前企业报名岗位响应 */
|
||||
export interface CompanyJobsResult {
|
||||
code: number;
|
||||
msg?: string;
|
||||
data?: JobInfo[];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user