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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -13,4 +13,6 @@ src/.umi-production/
|
||||
|
||||
# Archives
|
||||
*.rar
|
||||
*.zip
|
||||
*.zip
|
||||
.DS_Store
|
||||
.local/
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
*
|
||||
* @doc https://umijs.org/docs/guides/proxy
|
||||
*/
|
||||
const localBackendUrl = process.env.LOCAL_BACKEND_URL || 'http://127.0.0.1:9091';
|
||||
|
||||
export default {
|
||||
// 如果需要自定义本地开发服务器 请取消注释按需调整
|
||||
dev: {
|
||||
// localhost:8000/api/** -> https://preview.pro.ant.design/api/**
|
||||
'/api/': {
|
||||
// 要代理的地址
|
||||
target: 'http://localhost:9091', // 本地开发环境
|
||||
target: localBackendUrl,
|
||||
// target: 'http://39.98.44.136:6024/api/shihezi/', // 线上环境
|
||||
// target: 'http://wykj.cdwsx.com/api',// 后端
|
||||
// target: 'http://ks.zhaopinzao8dian.com/api/ks',
|
||||
@@ -26,7 +28,7 @@ export default {
|
||||
// logLevel: 'debug',
|
||||
},
|
||||
'/profile/avatar/': {
|
||||
target: 'http://localhost:9091', // 本地开发环境
|
||||
target: localBackendUrl,
|
||||
// target: 'http://36.105.163.21:30081/api/ks', // 发版
|
||||
// target: 'http://wykj.cdwsx.com/api',
|
||||
// target: 'http://ks.zhaopinzao8dian.com/api/ks',
|
||||
@@ -34,7 +36,7 @@ export default {
|
||||
},
|
||||
'/app/': {
|
||||
// 要代理的地址
|
||||
target: 'http://localhost:9091', // 本地开发环境
|
||||
target: localBackendUrl,
|
||||
// target: 'http://36.105.163.21:30081/api/ks', // 发版
|
||||
// target: 'http://wykj.cdwsx.com/api',
|
||||
// target: 'http://ks.zhaopinzao8dian.com/api/ks',
|
||||
|
||||
@@ -209,17 +209,17 @@ export default [
|
||||
{
|
||||
name: '公共招聘会列表',
|
||||
path: '/jobfair/public-job-fair',
|
||||
component: './Jobfair/Publicjobfair',
|
||||
component: './Jobfair/PublicJobFair',
|
||||
},
|
||||
{
|
||||
name: '招聘会详情',
|
||||
path: '/jobfair/public-job-fair/detail',
|
||||
component: './Jobfair/Publicjobfair/Detail',
|
||||
component: './Jobfair/PublicJobFair/Detail',
|
||||
},
|
||||
{
|
||||
name: '报名人员',
|
||||
path: '/jobfair/public-job-fair/signups',
|
||||
component: './Jobfair/Publicjobfair/Signups',
|
||||
component: './Jobfair/PublicJobFair/Signups',
|
||||
},
|
||||
{
|
||||
name: '户外招聘会管理',
|
||||
|
||||
231
local.sh
Executable file
231
local.sh
Executable file
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
RUNTIME_DIR="${RUNTIME_DIR:-${ROOT_DIR}/.local}"
|
||||
LOG_DIR="${RUNTIME_DIR}/logs"
|
||||
LOG_FILE="${LOG_FILE:-${LOG_DIR}/frontend.log}"
|
||||
PID_FILE="${PID_FILE:-${RUNTIME_DIR}/frontend.pid}"
|
||||
|
||||
BACKEND_URL="${LOCAL_BACKEND_URL:-http://127.0.0.1:9091}"
|
||||
FRONTEND_PORT="${PORT:-8000}"
|
||||
NPM_CMD="${NPM_CMD:-npm}"
|
||||
MAX_CMD="${ROOT_DIR}/node_modules/.bin/max"
|
||||
|
||||
info() {
|
||||
printf '\033[1;32m%s\033[0m\n' "$*"
|
||||
}
|
||||
|
||||
error() {
|
||||
printf '\033[1;31m%s\033[0m\n' "$*" >&2
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
用法:./local.sh <命令> [参数]
|
||||
|
||||
命令:
|
||||
install 使用 package-lock.json 安装依赖
|
||||
start [--skip-install] 检查本地后端并在后台启动前端
|
||||
stop 停止本脚本启动的前端进程
|
||||
restart [--skip-install] 重启前端
|
||||
status 查看前端和本地后端状态
|
||||
logs [行数] 查看并持续跟踪日志,默认显示最后 200 行
|
||||
help 显示帮助
|
||||
|
||||
可选环境变量:
|
||||
LOCAL_BACKEND_URL 本地后端地址,默认 http://127.0.0.1:9091
|
||||
PORT 前端端口,默认 8000
|
||||
NPM_CMD npm 命令,默认 npm
|
||||
EOF
|
||||
}
|
||||
|
||||
prepare_runtime() {
|
||||
mkdir -p "${LOG_DIR}"
|
||||
}
|
||||
|
||||
read_pid() {
|
||||
if [[ -f "${PID_FILE}" ]]; then
|
||||
tr -d '[:space:]' < "${PID_FILE}"
|
||||
fi
|
||||
}
|
||||
|
||||
is_running() {
|
||||
local pid
|
||||
pid="$(read_pid)"
|
||||
[[ -n "${pid}" ]] && kill -0 "${pid}" 2>/dev/null
|
||||
}
|
||||
|
||||
install_dependencies() {
|
||||
command -v "${NPM_CMD}" >/dev/null 2>&1 || {
|
||||
error "未找到 npm 命令:${NPM_CMD}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
info "正在根据 package-lock.json 安装依赖..."
|
||||
(
|
||||
cd "${ROOT_DIR}"
|
||||
"${NPM_CMD}" ci
|
||||
)
|
||||
}
|
||||
|
||||
check_backend() {
|
||||
local status
|
||||
command -v curl >/dev/null 2>&1 || {
|
||||
error "未找到 curl,无法检查本地后端"
|
||||
exit 1
|
||||
}
|
||||
|
||||
status="$(curl -s -o /dev/null --max-time 3 -w '%{http_code}' "${BACKEND_URL}/" || true)"
|
||||
if [[ -z "${status}" || "${status}" == "000" ]]; then
|
||||
error "本地后端不可访问:${BACKEND_URL}"
|
||||
error "请先在 shz-backend 中执行:./local.sh start"
|
||||
exit 1
|
||||
fi
|
||||
info "本地后端已连接:${BACKEND_URL}(HTTP ${status})"
|
||||
}
|
||||
|
||||
start_frontend() {
|
||||
local skip_install="${1:-}"
|
||||
|
||||
prepare_runtime
|
||||
if is_running; then
|
||||
info "前端已在运行,PID:$(read_pid)"
|
||||
return 0
|
||||
fi
|
||||
rm -f "${PID_FILE}"
|
||||
|
||||
check_backend
|
||||
|
||||
if [[ ! -x "${MAX_CMD}" ]]; then
|
||||
if [[ "${skip_install}" == "--skip-install" ]]; then
|
||||
error "依赖尚未安装,不能跳过安装:${MAX_CMD} 不存在"
|
||||
exit 1
|
||||
fi
|
||||
install_dependencies
|
||||
elif [[ -n "${skip_install}" && "${skip_install}" != "--skip-install" ]]; then
|
||||
error "未知参数:${skip_install}"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
export REACT_APP_ENV=dev
|
||||
export MOCK=none
|
||||
export UMI_ENV=dev
|
||||
export LOCAL_BACKEND_URL="${BACKEND_URL}"
|
||||
export PORT="${FRONTEND_PORT}"
|
||||
|
||||
info "正在启动前端:http://localhost:${FRONTEND_PORT}"
|
||||
info "后端代理目标:${BACKEND_URL}"
|
||||
(
|
||||
cd "${ROOT_DIR}"
|
||||
nohup "${MAX_CMD}" dev >> "${LOG_FILE}" 2>&1 &
|
||||
printf '%s\n' "$!" > "${PID_FILE}"
|
||||
)
|
||||
|
||||
local frontend_status="000"
|
||||
for _ in {1..60}; do
|
||||
if ! is_running; then
|
||||
break
|
||||
fi
|
||||
frontend_status="$(curl -s -o /dev/null --max-time 1 -w '%{http_code}' \
|
||||
"http://127.0.0.1:${FRONTEND_PORT}/" || true)"
|
||||
if [[ -n "${frontend_status}" && "${frontend_status}" != "000" ]]; then
|
||||
info "前端已启动,PID:$(read_pid),HTTP ${frontend_status}"
|
||||
info "日志命令:./local.sh logs"
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
rm -f "${PID_FILE}"
|
||||
error "前端未能在 60 秒内启动,最近日志如下:"
|
||||
tail -n 80 "${LOG_FILE}" >&2 || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
stop_frontend() {
|
||||
local pid
|
||||
pid="$(read_pid)"
|
||||
|
||||
if ! is_running; then
|
||||
rm -f "${PID_FILE}"
|
||||
info "前端未运行"
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "正在停止前端,PID:${pid}"
|
||||
kill "${pid}"
|
||||
for _ in {1..15}; do
|
||||
if ! kill -0 "${pid}" 2>/dev/null; then
|
||||
rm -f "${PID_FILE}"
|
||||
info "前端已停止"
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
error "前端未在 15 秒内停止,请检查 PID:${pid}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
show_status() {
|
||||
local backend_status
|
||||
if is_running; then
|
||||
info "前端正在运行,PID:$(read_pid),地址:http://localhost:${FRONTEND_PORT}"
|
||||
else
|
||||
rm -f "${PID_FILE}"
|
||||
info "前端未运行"
|
||||
fi
|
||||
|
||||
backend_status="$(curl -s -o /dev/null --max-time 2 -w '%{http_code}' "${BACKEND_URL}/" || true)"
|
||||
if [[ -n "${backend_status}" && "${backend_status}" != "000" ]]; then
|
||||
info "本地后端可访问:${BACKEND_URL}(HTTP ${backend_status})"
|
||||
else
|
||||
error "本地后端不可访问:${BACKEND_URL}"
|
||||
fi
|
||||
info "日志文件:${LOG_FILE}"
|
||||
}
|
||||
|
||||
show_logs() {
|
||||
local lines="${1:-200}"
|
||||
if [[ ! "${lines}" =~ ^[0-9]+$ ]]; then
|
||||
error "日志行数必须是非负整数:${lines}"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
prepare_runtime
|
||||
touch "${LOG_FILE}"
|
||||
info "正在跟踪日志:${LOG_FILE}(Ctrl+C 退出)"
|
||||
tail -n "${lines}" -f "${LOG_FILE}"
|
||||
}
|
||||
|
||||
command="${1:-help}"
|
||||
case "${command}" in
|
||||
install)
|
||||
install_dependencies
|
||||
;;
|
||||
start)
|
||||
start_frontend "${2:-}"
|
||||
;;
|
||||
stop)
|
||||
stop_frontend
|
||||
;;
|
||||
restart)
|
||||
stop_frontend
|
||||
start_frontend "${2:-}"
|
||||
;;
|
||||
status)
|
||||
show_status
|
||||
;;
|
||||
logs)
|
||||
show_logs "${2:-200}"
|
||||
;;
|
||||
help|-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
error "未知命令:${command}"
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -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':
|
||||
|
||||
@@ -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' }}>支持 jpg、jpeg、png、gif、bmp,大小不超过 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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface OutdoorFairListParams {
|
||||
title?: string;
|
||||
hostUnit?: string;
|
||||
fairType?: string;
|
||||
region?: string;
|
||||
current?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
@@ -70,177 +71,54 @@ export interface OutdoorFairCommonResult {
|
||||
code: number;
|
||||
msg?: string;
|
||||
data?: any;
|
||||
url?: string;
|
||||
fileName?: string;
|
||||
newFileName?: string;
|
||||
originalFilename?: string;
|
||||
}
|
||||
|
||||
// ==================== 静态假数据 ====================
|
||||
|
||||
const MOCK_DATA: OutdoorFairItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: '2026年石河子市春季户外招聘会',
|
||||
hostUnit: '第八师石河子市人力资源和社会保障局',
|
||||
fairType: '综合类',
|
||||
region: '第八师(石河子市)',
|
||||
address: '石河子市北三路石河子大学中区图书馆前广场',
|
||||
boothCount: 80,
|
||||
holdTime: '2026-03-15 09:00:00',
|
||||
endTime: '2026-03-15 17:00:00',
|
||||
applyStartTime: '2026-03-01 00:00:00',
|
||||
applyEndTime: '2026-03-10 23:59:59',
|
||||
onlineApply: true,
|
||||
photoUrl: 'https://picsum.photos/seed/outdoor1/400/300',
|
||||
createTime: '2026-02-20 10:30:00',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '2026年阿拉尔市高校户外专场招聘会',
|
||||
hostUnit: '第一师阿拉尔市人力资源和社会保障局',
|
||||
fairType: '校园招聘',
|
||||
region: '第一师(阿拉尔市)',
|
||||
address: '阿拉尔市塔里木大道塔里木大学综合教学楼前广场',
|
||||
boothCount: 60,
|
||||
holdTime: '2026-04-20 09:00:00',
|
||||
endTime: '2026-04-20 16:00:00',
|
||||
applyStartTime: '2026-04-01 00:00:00',
|
||||
applyEndTime: '2026-04-15 23:59:59',
|
||||
onlineApply: true,
|
||||
photoUrl: 'https://picsum.photos/seed/outdoor2/400/300',
|
||||
createTime: '2026-03-01 14:00:00',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '2026年图木舒克市制造业户外招聘会',
|
||||
hostUnit: '第三师图木舒克市人力资源和社会保障局',
|
||||
fairType: '行业专场',
|
||||
region: '第三师(图木舒克市)',
|
||||
address: '图木舒克市迎宾大道市政广场',
|
||||
boothCount: 120,
|
||||
holdTime: '2026-05-10 08:30:00',
|
||||
endTime: '2026-05-10 17:30:00',
|
||||
applyStartTime: '2026-04-25 00:00:00',
|
||||
applyEndTime: '2026-05-05 23:59:59',
|
||||
onlineApply: false,
|
||||
photoUrl: 'https://picsum.photos/seed/outdoor3/400/300',
|
||||
createTime: '2026-03-10 09:00:00',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '2026年可克达拉市高新技术户外招聘会',
|
||||
hostUnit: '第四师可克达拉市人力资源和社会保障局',
|
||||
fairType: '高新技术专场',
|
||||
region: '第四师(可克达拉市)',
|
||||
address: '可克达拉市镇江路市民服务中心广场',
|
||||
boothCount: 50,
|
||||
holdTime: '2026-06-18 09:00:00',
|
||||
endTime: '2026-06-18 16:30:00',
|
||||
applyStartTime: '2026-06-01 00:00:00',
|
||||
applyEndTime: '2026-06-12 23:59:59',
|
||||
onlineApply: true,
|
||||
photoUrl: 'https://picsum.photos/seed/outdoor4/400/300',
|
||||
createTime: '2026-03-20 11:00:00',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '2026年双河市农业行业户外招聘会',
|
||||
hostUnit: '第五师双河市人力资源和社会保障局',
|
||||
fairType: '行业专场',
|
||||
region: '第五师(双河市)',
|
||||
address: '双河市人民路双河市文化广场',
|
||||
boothCount: 40,
|
||||
holdTime: '2026-07-08 09:00:00',
|
||||
endTime: '2026-07-08 17:00:00',
|
||||
applyStartTime: '2026-06-20 00:00:00',
|
||||
applyEndTime: '2026-07-03 23:59:59',
|
||||
onlineApply: false,
|
||||
photoUrl: 'https://picsum.photos/seed/outdoor5/400/300',
|
||||
createTime: '2026-04-01 08:30:00',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: '2026年五家渠市电子产业户外招聘会',
|
||||
hostUnit: '第六师五家渠市人力资源和社会保障局',
|
||||
fairType: '行业专场',
|
||||
region: '第六师(五家渠市)',
|
||||
address: '五家渠市长安东街五家渠市人民广场',
|
||||
boothCount: 70,
|
||||
holdTime: '2026-08-15 08:30:00',
|
||||
endTime: '2026-08-15 17:30:00',
|
||||
applyStartTime: '2026-08-01 00:00:00',
|
||||
applyEndTime: '2026-08-10 23:59:59',
|
||||
onlineApply: true,
|
||||
photoUrl: 'https://picsum.photos/seed/outdoor6/400/300',
|
||||
createTime: '2026-04-15 10:00:00',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: '2026年胡杨河市智能制造户外招聘会',
|
||||
hostUnit: '第七师胡杨河市人力资源和社会保障局',
|
||||
fairType: '智能制造专场',
|
||||
region: '第七师(胡杨河市)',
|
||||
address: '胡杨河市团结路胡杨河市创业广场',
|
||||
boothCount: 55,
|
||||
holdTime: '2026-09-12 09:00:00',
|
||||
endTime: '2026-09-12 16:00:00',
|
||||
applyStartTime: '2026-08-25 00:00:00',
|
||||
applyEndTime: '2026-09-05 23:59:59',
|
||||
onlineApply: true,
|
||||
photoUrl: 'https://picsum.photos/seed/outdoor7/400/300',
|
||||
createTime: '2026-05-01 09:30:00',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: '2026年北屯市文旅行业户外招聘会',
|
||||
hostUnit: '第十师北屯市人力资源和社会保障局',
|
||||
fairType: '文旅专场',
|
||||
region: '第十师(北屯市)',
|
||||
address: '北屯市博望路北屯市文化广场',
|
||||
boothCount: 35,
|
||||
holdTime: '2026-10-20 09:00:00',
|
||||
endTime: '2026-10-20 17:00:00',
|
||||
applyStartTime: '2026-10-01 00:00:00',
|
||||
applyEndTime: '2026-10-15 23:59:59',
|
||||
onlineApply: false,
|
||||
photoUrl: 'https://picsum.photos/seed/outdoor8/400/300',
|
||||
createTime: '2026-05-10 16:00:00',
|
||||
},
|
||||
];
|
||||
|
||||
// 自增 ID
|
||||
let nextId = MOCK_DATA.length + 1;
|
||||
|
||||
// ==================== API 接口(使用假数据) ====================
|
||||
/** 户外招聘会字典新增表单 */
|
||||
export interface OutdoorFairDictForm {
|
||||
dictType: 'outdoor_fair_type' | 'outdoor_fair_region';
|
||||
dictLabel: string;
|
||||
}
|
||||
|
||||
const CMS_OUTDOOR_FAIR_BASE = '/api/cms/outdoor-fair';
|
||||
|
||||
const formatDateTime = (value: any) => {
|
||||
if (value && typeof value.format === 'function') {
|
||||
return value.format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const toApiPayload = (data: OutdoorFairForm) => ({
|
||||
...data,
|
||||
holdTime: formatDateTime(data.holdTime),
|
||||
endTime: formatDateTime(data.endTime),
|
||||
applyStartTime: formatDateTime(data.applyStartTime),
|
||||
applyEndTime: formatDateTime(data.applyEndTime),
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取户外招聘会管理列表
|
||||
* GET /api/cms/outdoor-fair/list
|
||||
*/
|
||||
export async function getOutdoorFairList(params?: OutdoorFairListParams) {
|
||||
let filtered = [...MOCK_DATA];
|
||||
return request<OutdoorFairListResult>(`${CMS_OUTDOOR_FAIR_BASE}/list`, {
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
if (params?.title) {
|
||||
filtered = filtered.filter((item) => item.title.includes(params.title!));
|
||||
}
|
||||
if (params?.hostUnit) {
|
||||
filtered = filtered.filter((item) => item.hostUnit.includes(params.hostUnit!));
|
||||
}
|
||||
if (params?.fairType) {
|
||||
filtered = filtered.filter((item) => item.fairType.includes(params.fairType!));
|
||||
}
|
||||
|
||||
const total = filtered.length;
|
||||
const current = params?.current || 1;
|
||||
const pageSize = params?.pageSize || 10;
|
||||
const start = (current - 1) * pageSize;
|
||||
const rows = filtered.slice(start, start + pageSize);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
msg: '操作成功',
|
||||
total,
|
||||
rows,
|
||||
};
|
||||
/**
|
||||
* 获取户外招聘会详情
|
||||
* GET /api/cms/outdoor-fair/{id}
|
||||
*/
|
||||
export async function getOutdoorFairInfo(id: number) {
|
||||
return request<OutdoorFairCommonResult>(`${CMS_OUTDOOR_FAIR_BASE}/${id}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -248,13 +126,10 @@ export async function getOutdoorFairList(params?: OutdoorFairListParams) {
|
||||
* POST /api/cms/outdoor-fair
|
||||
*/
|
||||
export async function addOutdoorFair(data: OutdoorFairForm) {
|
||||
const newItem: OutdoorFairItem = {
|
||||
...data,
|
||||
id: nextId++,
|
||||
createTime: new Date().toISOString().replace('T', ' ').substring(0, 19),
|
||||
};
|
||||
MOCK_DATA.unshift(newItem);
|
||||
return { code: 200, msg: '新增成功', data: newItem };
|
||||
return request<OutdoorFairCommonResult>(CMS_OUTDOOR_FAIR_BASE, {
|
||||
method: 'POST',
|
||||
data: toApiPayload(data),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -262,16 +137,10 @@ export async function addOutdoorFair(data: OutdoorFairForm) {
|
||||
* PUT /api/cms/outdoor-fair
|
||||
*/
|
||||
export async function updateOutdoorFair(data: OutdoorFairForm) {
|
||||
const index = MOCK_DATA.findIndex((item) => item.id === data.id);
|
||||
if (index !== -1) {
|
||||
MOCK_DATA[index] = {
|
||||
...MOCK_DATA[index],
|
||||
...data,
|
||||
updateTime: new Date().toISOString().replace('T', ' ').substring(0, 19),
|
||||
};
|
||||
return { code: 200, msg: '修改成功', data: MOCK_DATA[index] };
|
||||
}
|
||||
return { code: 404, msg: '未找到该记录' };
|
||||
return request<OutdoorFairCommonResult>(CMS_OUTDOOR_FAIR_BASE, {
|
||||
method: 'PUT',
|
||||
data: toApiPayload(data),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,10 +148,31 @@ export async function updateOutdoorFair(data: OutdoorFairForm) {
|
||||
* DELETE /api/cms/outdoor-fair/{id}
|
||||
*/
|
||||
export async function deleteOutdoorFair(id: number) {
|
||||
const index = MOCK_DATA.findIndex((item) => item.id === id);
|
||||
if (index !== -1) {
|
||||
MOCK_DATA.splice(index, 1);
|
||||
return { code: 200, msg: '删除成功' };
|
||||
}
|
||||
return { code: 404, msg: '未找到该记录' };
|
||||
return request<OutdoorFairCommonResult>(`${CMS_OUTDOOR_FAIR_BASE}/${id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增户外招聘会页面使用的字典项
|
||||
* POST /api/cms/outdoor-fair/dict-data
|
||||
*/
|
||||
export async function addOutdoorFairDictOption(data: OutdoorFairDictForm) {
|
||||
return request<OutdoorFairCommonResult>(`${CMS_OUTDOOR_FAIR_BASE}/dict-data`, {
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传户外招聘会照片
|
||||
* POST /api/common/upload
|
||||
*/
|
||||
export async function uploadOutdoorFairPhoto(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return request<OutdoorFairCommonResult>('/api/common/upload', {
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user