yintai-company-home/src/pages/Join/Campus.tsx

215 lines
7.7 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import styles from "./Campus.module.css";
import Banner, { type BannerConfig } from "@/components/Banner";
import Section from "@/components/layout/Section";
import { useStore } from "@/store";
import { SearchOutlined, FilterOutlined, DownOutlined, RightOutlined } from "@ant-design/icons";
import { Empty, Select } from "antd";
import Pagination from "@/components/Pagination";
import { Link } from "react-router-dom";
import { debounce } from "@/utils";
import appApi from "@/api/app";
type JobItem = {
id: number;
title: string;
content: string;
labels: string[];
lang: string
}
export default function JoinCampus() {
const appConfig = useStore((s) => s.appConfig);
const supportLocales = useStore((s) => s.supportLocales);
const locale = useStore((s) => s.locale);
const data = appConfig?.join?.campus;
const banner = data?.banner;
const [searchValue, setSearchValue] = useState('');
// 职业类别 业务领域 所属板块
const [jobType, setJobType] = useState('');
const [jobTypeOptions, setJobTypeOptions] = useState([]);
const [businessArea, setBusinessArea] = useState('');
const [businessAreaOptions, setBusinessAreaOptions] = useState([]);
const [businessPlate, setBusinessPlate] = useState('');
const [businessPlateOptions, setBusinessPlateOptions] = useState([]);
const [page, setPage] = useState(1);
const [size] = useState(2 * supportLocales.length);
const [total, setTotal] = useState(1000);
const [jobList, setJobList] = useState<JobItem[]>([]);
const localJobList = useMemo(() => {
return jobList.filter(item => item.lang.toLowerCase() === locale.split('-')[0]);
}, [jobList, locale]);
// 用 ref 保存最新参数,保证 debounce 使用稳定的函数引用
const paramsRef = useRef({ searchValue, jobType, businessArea, businessPlate, page, size });
paramsRef.current = { searchValue, jobType, businessArea, businessPlate, page, size };
const refreshData = useMemo(() => debounce(() => {
const { searchValue: sv, jobType: jt, businessArea: ba, businessPlate: bp, page: p, size: s } = paramsRef.current;
appApi.getJobList({ page: p, size: s, sort: "", title: sv, job_type: jt, business_area: ba, business_plate: bp }).then((res) => {
const items = res.data.items.map((item: any) => {
const { job_type_name, job_area_name, job_unit_name } = item.category;
const labels = [job_type_name, job_area_name, job_unit_name];
return {
id: item.id,
title: item.title,
content: item.description,
labels,
lang: item.lang,
};
});
setJobList(items || []);
setTotal(res.data.total);
});
}, 500), []);
const getTypes = useCallback(() => {
['job_type', 'job_area', 'job_unit'].forEach(type => {
appApi.getCategoryList(type).then((res) => {
const items = res.data.items.map((item:any) => ({ label: item.name, value: item.id }));
items.unshift({ label: "全部", value: "" });
if (type === 'job_type') {
setJobTypeOptions(items);
} else if (type === 'job_area') {
setBusinessAreaOptions(items);
} else if (type === 'job_unit') {
setBusinessPlateOptions(items);
}
})
})
}, []);
useEffect(() => {
refreshData();
}, [searchValue, jobType, businessArea, businessPlate, page, size]);
useEffect(() => {
getTypes();
}, []);
const handleReset = useCallback(() => {
setSearchValue('');
setJobType('');
setBusinessArea('');
setBusinessPlate('');
setPage(1);
}, []);
return (
<div>
<Banner
title={banner?.title ?? "招贤纳士"}
content={(banner as BannerConfig)?.content}
titleSize={(banner as BannerConfig)?.titleSize ?? "large"}
backgroundImage={banner?.backgroundImage ?? "/images/bg-overview.png"}
/>
<Section maskBackground="#FFFFFF">
<div className={styles.campusRow}>
<div className={styles.campusColLeft}>
<SearchInput value={searchValue} placeholder="搜索职位" onChange={setSearchValue} onEnter={refreshData} />
<SelectFormItem value={jobType} options={jobTypeOptions} label="职业类别" onChange={setJobType} />
<SelectFormItem value={businessArea} options={businessAreaOptions} label="业务领域" onChange={setBusinessArea} />
<SelectFormItem value={businessPlate} options={businessPlateOptions} label="所属板块" onChange={setBusinessPlate} />
<button className={styles.resetButton} onClick={handleReset}></button>
</div>
<div className={styles.campusColRight}>
<div className={styles.jobList}>
{/* 没有数据时显示 */}
{localJobList.length === 0 && (
<div className={styles.noData}>
<Empty description="暂无数据" />
</div>
)}
{localJobList.map((item, index) => (
<Link key={item.id} to={`/join/campus/detail/${item.id}`}>
<div className={styles.jobItem}>
<div className={styles.jobItemTitleRow}>
<div className={styles.jobItemTitle}>{item.title}</div>
<div className={styles.jobItemTitleRight}> <RightOutlined /></div>
</div>
<div className={styles.jobItemLabels}>
{item.labels.map((label, index) => (
<div key={index} className={styles.jobItemLabel}>&nbsp;&nbsp;&nbsp;{label}</div>
))}
</div>
<div className={styles.jobItemContent}>{item.content}</div>
</div></Link>
))}
</div>
<Pagination total={total} size={size} page={page} onChange={setPage} />
</div>
</div>
</Section>
</div>
);
}
type SearchInputProps = {
value: string;
placeholder: string;
onChange: (value: string) => void;
onEnter: () => void;
}
function SearchInput(
{ value, placeholder, onChange, onEnter }: SearchInputProps) {
return (
<div className={styles.searchInput}>
<label htmlFor="searchInput"><SearchOutlined className={styles.searchIcon} /></label>
<input
id="searchInput"
type="text"
placeholder={placeholder}
value={value}
onChange={e => onChange(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') {
onEnter();
}
}}
/>
</div>
);
}
type SelectFormItemProps = {
value: string;
options: { label: string; value: string }[];
label: string;
onChange: (value: string) => void;
}
function SelectFormItem({ value, options, label, onChange }: SelectFormItemProps) {
return (
<div className={styles.selectFormItem}>
<label className={styles.selectFormItemLabel} htmlFor={`select-${label.replace(/\s/g, "-")}`}>{label}</label>
<div className={styles.selectFormItemBox}>
<Select
id={`select-${label.replace(/\s/g, "-")}`}
className={styles.selectFormItemSelect}
showSearch
placeholder="全部"
notFoundContent="无数据"
optionFilterProp="label"
filterOption={(input, opt) =>
(opt?.label ?? "").toString().toLowerCase().includes(input.toLowerCase())
}
options={options}
value={value}
onChange={v => onChange(v ?? "")}
prefix={<FilterOutlined className={styles.selectFilterIcon} />}
suffixIcon={<DownOutlined className={styles.selectDownIcon} />}
bordered={false}
/>
</div>
</div>
);
}