215 lines
7.7 KiB
TypeScript
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}> • {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>
|
|
);
|
|
}
|