diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 352069c..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index 61a7226..edc834e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,6 @@ src/.umi-production/ # Archives *.rar -*.zip \ No newline at end of file +*.zip +.DS_Store +.local/ diff --git a/config/proxy.ts b/config/proxy.ts index 6d6cc1e..e7ab963 100644 --- a/config/proxy.ts +++ b/config/proxy.ts @@ -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', diff --git a/config/routes.ts b/config/routes.ts index 48a75d5..7ee0c81 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -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: '户外招聘会管理', diff --git a/local.sh b/local.sh new file mode 100755 index 0000000..77f5808 --- /dev/null +++ b/local.sh @@ -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 diff --git a/src/pages/Jobfair/Outdoorfair/Detail/index.tsx b/src/pages/Jobfair/Outdoorfair/Detail/index.tsx index c056554..59c9570 100644 --- a/src/pages/Jobfair/Outdoorfair/Detail/index.tsx +++ b/src/pages/Jobfair/Outdoorfair/Detail/index.tsx @@ -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 ; + return ( + + ); case 'booth': return ; case 'company-review': diff --git a/src/pages/Jobfair/Outdoorfair/components/EditModal.tsx b/src/pages/Jobfair/Outdoorfair/components/EditModal.tsx index f9a975f..b77aaf0 100644 --- a/src/pages/Jobfair/Outdoorfair/components/EditModal.tsx +++ b/src/pages/Jobfair/Outdoorfair/components/EditModal.tsx @@ -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; onCancel: () => void; onSubmit: (values: OutdoorFairForm) => Promise; } -/** 招聘会类型选项 */ -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; + 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 = ({ open, values, onCancel, onSubmit }) => { +const EditModal: React.FC = ({ + 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(); + const photoUrl = Form.useWatch('photoUrl', form); useEffect(() => { if (open) { @@ -60,6 +59,85 @@ const EditModal: React.FC = ({ 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} + + + + + + ); + + 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 ( = ({ open, values, onCancel, onSubmit }} > ); }; diff --git a/src/pages/Jobfair/Outdoorfair/index.tsx b/src/pages/Jobfair/Outdoorfair/index.tsx index 1719362..984929f 100644 --- a/src/pages/Jobfair/Outdoorfair/index.tsx +++ b/src/pages/Jobfair/Outdoorfair/index.tsx @@ -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(); const [modalVisible, setModalVisible] = useState(false); const [currentRow, setCurrentRow] = useState(); + const [fairTypeValueEnum, setFairTypeValueEnum] = useState>({}); + const [fairTypeOptions, setFairTypeOptions] = useState([]); + const [regionValueEnum, setRegionValueEnum] = useState>({}); + const [regionOptions, setRegionOptions] = useState([]); + + 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 = () => { { 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); diff --git a/src/services/jobportal/outdoorFair.ts b/src/services/jobportal/outdoorFair.ts index efc6f5d..336b6d7 100644 --- a/src/services/jobportal/outdoorFair.ts +++ b/src/services/jobportal/outdoorFair.ts @@ -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(`${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(`${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(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(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(`${CMS_OUTDOOR_FAIR_BASE}/${id}`, { + method: 'DELETE', + }); +} + +/** + * 新增户外招聘会页面使用的字典项 + * POST /api/cms/outdoor-fair/dict-data + */ +export async function addOutdoorFairDictOption(data: OutdoorFairDictForm) { + return request(`${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('/api/common/upload', { + method: 'POST', + data: formData, + }); }