import { Ionicons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { useRouter } from 'expo-router'; import { useState } from 'react'; import { ActivityIndicator, Modal, Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useJobList } from '../../src/hooks'; import { ALL_CITY_LABEL, CITIES, EDUCATION_OPTIONS, EXPERIENCE_OPTIONS, JOB_LEVEL_LABEL, SALARY_RANGES, } from '../../src/constants'; import type { Job, SortType } from '../../src/types'; import { colors, headerGradient } from '../../src/theme'; /** 筛选项(下拉占位) */ const FILTERS = ['全部行业', '薪资', '经验', '更多筛选']; /** 排序选项 */ const SORTS: { key: SortType; label: string }[] = [ { key: 'comprehensive', label: '综合排序' }, { key: 'latest', label: '最新发布' }, { key: 'salary', label: '薪资最高' }, ]; /** 等级 → 文字颜色 */ const LEVEL_COLOR = { excellent: colors.primaryDark, good: colors.primary, medium: colors.textSecondary, } as const; /** 更多筛选的三组字段 */ type FilterKey = 'salary' | 'experience' | 'education'; /** 切换某个筛选值(多选:已在则移除,否则加入) */ function toggleValue( draft: Record, key: FilterKey, value: string, ): Record { const current = draft[key]; const next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value]; return { ...draft, [key]: next }; } /** 清空某一组筛选 */ function clearGroup(draft: Record, key: FilterKey): Record { return { ...draft, [key]: [] }; } /** 筛选面板中的一组选项(「不限」为清空态) */ function FilterGroup({ title, options, selected, onToggle, onClear, }: { title: string; options: string[]; selected: string[]; onToggle: (value: string) => void; onClear: () => void; }) { return ( <> {title} {options.map((option) => { const isNone = option === '不限'; const active = isNone ? selected.length === 0 : selected.includes(option); return ( (isNone ? onClear() : onToggle(option))} > {option} ); })} ); } /** 单张岗位卡片(整卡可点击进入详情) */ function JobCard({ job }: { job: Job }) { const router = useRouter(); return ( [styles.card, pressed && styles.cardPressed]} onPress={() => router.push(`/jobs/${job.id}`)} > {/* 公司名 + 地点 */} {job.companyName} {job.companyLocation} {/* 岗位名 + 薪资 */} {job.positionName} {job.salaryMin}-{job.salaryMax}K {/* 评分标签 + 查看详情 */} {job.score}分 · {JOB_LEVEL_LABEL[job.level]} 查看详情 > {/* 福利标签 */} {[job.experience, job.education, ...job.benefits].map((item) => ( {item} ))} ); } /** * 岗位列表页 */ export default function JobsScreen() { const router = useRouter(); const insets = useSafeAreaInsets(); // 城市筛选(点击可切换)。默认「全部城市」:此前写死「北京」, // 等于把每个用户都钉在北京,引导里填的期望城市永远影响不到推荐 const [city, setCity] = useState(ALL_CITY_LABEL); const [cityVisible, setCityVisible] = useState(false); const [keyword, setKeyword] = useState(''); const [sort, setSort] = useState('comprehensive'); // 更多筛选:已应用的筛选条件(多选,空数组 = 不限) const [filters, setFilters] = useState>({ salary: [], experience: [], education: [], }); // 筛选面板内的临时选择(点「确定」后才生效) const [draft, setDraft] = useState>(filters); const [filterVisible, setFilterVisible] = useState(false); const { jobs, total, loading, error, refresh, isEmpty } = useJobList({ sortBy: sort, city, salary: filters.salary, experience: filters.experience, education: filters.education, }); // 已勾选的筛选项总数(用于「更多筛选」角标) const activeCount = filters.salary.length + filters.experience.length + filters.education.length; return ( {/* 顶部渐变区:状态栏 + 导航 + 搜索 */} {/* 顶部导航:定位城市 + 标题 + 评分权重设置 */} setCityVisible(true)}> {city} 找工作 { router.push('/jobs/weight-settings'); }} hitSlop={6} > {/* 搜索框 */} {/* 筛选条件栏(横向滚动) */} {FILTERS.map((filter) => ( { if (filter === '更多筛选') { setDraft(filters); setFilterVisible(true); } /* 其余筛选(行业/城市/薪资/经验)留待后续接入 */ }} > {filter} {filter === '更多筛选' && activeCount > 0 && ( {activeCount} )} ))} {/* 结果统计 + 排序 + 岗位对比 */} 共找到 {total} 个岗位 {SORTS.map((option) => ( setSort(option.key)}> {option.label} ))} { router.push('/jobs/compare'); }} hitSlop={6} > 对比 {/* 岗位列表:统一处理加载 / 错误 / 空 / 成功四态 */} {loading ? ( ) : error ? ( 加载失败,请重试 重试 ) : isEmpty ? ( 暂无相关岗位 ) : ( {jobs.map((job) => ( ))} )} {/* 更多筛选面板(底部弹层) */} setFilterVisible(false)} > setFilterVisible(false)}> {/* affordance-ok:吞冒泡,点弹层内容区不关闭 */} {}}> 更多筛选 setFilterVisible(false)}> {/* 薪资范围 */} range.label)} selected={draft.salary} onToggle={(value) => setDraft((d) => toggleValue(d, 'salary', value))} onClear={() => setDraft((d) => clearGroup(d, 'salary'))} /> {/* 经验要求 */} setDraft((d) => toggleValue(d, 'experience', value))} onClear={() => setDraft((d) => clearGroup(d, 'experience'))} /> {/* 学历要求 */} setDraft((d) => toggleValue(d, 'education', value))} onClear={() => setDraft((d) => clearGroup(d, 'education'))} /> {/* 底部操作 */} setDraft({ salary: [], experience: [], education: [] })} > 重置 { setFilters(draft); setFilterVisible(false); }} > 确定 {/* 城市选择弹层(底部弹层) */} setCityVisible(false)} > setCityVisible(false)}> {/* affordance-ok:吞冒泡,点弹层内容区不关闭 */} {}}> 选择城市 setCityVisible(false)}> {/* 「全部城市」是默认值与复位入口:不给它出口,用户选了城市就再也回不到不限 */} {[ALL_CITY_LABEL, ...CITIES].map((item) => ( { setCity(item); setCityVisible(false); }} > {item} ))} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.pageBg, }, headerGradient: { paddingBottom: 16, }, nav: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8, paddingTop: 4, paddingBottom: 8, }, navSide: { flex: 1, flexDirection: 'row', alignItems: 'center', }, locationBtn: { flexDirection: 'row', alignItems: 'center', gap: 2, }, locationText: { fontSize: 15, fontWeight: '600', color: colors.white, }, navTitle: { fontSize: 18, fontWeight: '700', color: colors.white, }, searchBox: { flexDirection: 'row', alignItems: 'center', height: 40, borderRadius: 20, backgroundColor: colors.white, paddingHorizontal: 14, marginHorizontal: 8, marginTop: 4, shadowColor: colors.shadow, shadowOpacity: 0.06, shadowRadius: 8, shadowOffset: { width: 0, height: 2 }, elevation: 2, }, searchInput: { flex: 1, marginLeft: 8, fontSize: 14, color: colors.textPrimary, }, filterBar: { flexGrow: 0, height: 32, marginTop: 12, }, filterContent: { paddingHorizontal: 8, gap: 12, }, filterChip: { flexDirection: 'row', alignItems: 'center', height: 32, paddingHorizontal: 12, borderRadius: 16, backgroundColor: colors.white, borderWidth: 1, borderColor: colors.border, }, filterText: { marginRight: 4, fontSize: 13, color: colors.textPrimary, }, filterBadge: { marginLeft: 4, minWidth: 16, height: 16, borderRadius: 8, paddingHorizontal: 4, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.textPrimary, }, filterBadgeText: { fontSize: 10, fontWeight: '700', color: colors.white, }, resultRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 8, marginTop: 16, marginBottom: 12, }, resultCount: { fontSize: 12, color: colors.textSecondary, }, sortRow: { flexDirection: 'row', alignItems: 'center', gap: 12, }, sortText: { fontSize: 13, color: colors.textSecondary, }, sortTextActive: { color: colors.textPrimary, fontWeight: '700', }, sortDivider: { width: StyleSheet.hairlineWidth, height: 12, backgroundColor: colors.border, marginHorizontal: 2, }, compareBtn: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 12, backgroundColor: colors.primaryLight, gap: 3, }, compareBtnText: { fontSize: 12, fontWeight: '600', color: colors.primaryText, }, navIconBtn: { width: 36, height: 36, alignItems: 'center', justifyContent: 'center', borderRadius: 18, backgroundColor: 'rgba(255,255,255,0.15)', }, stateBox: { flex: 1, alignItems: 'center', justifyContent: 'center', }, stateText: { marginTop: 12, fontSize: 14, color: colors.textSecondary, }, retryBtn: { marginTop: 16, paddingHorizontal: 24, paddingVertical: 8, borderRadius: 18, backgroundColor: colors.primaryBtn, }, retryText: { fontSize: 13, fontWeight: '600', color: colors.white, }, list: { flex: 1, }, listContent: { paddingHorizontal: 8, paddingBottom: 24, }, card: { backgroundColor: colors.white, borderRadius: 8, padding: 16, marginBottom: 8, }, cardPressed: { opacity: 0.85, }, cardHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, companyName: { fontSize: 16, fontWeight: '700', color: colors.textPrimary, }, companyLocation: { fontSize: 12, color: colors.textSecondary, }, positionRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginTop: 8, }, positionName: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, }, salary: { fontSize: 15, fontWeight: '700', color: colors.primary, }, scoreRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginTop: 10, }, scoreText: { fontSize: 13, fontWeight: '600', }, detailLink: { fontSize: 13, color: colors.primary, }, benefits: { flexDirection: 'row', flexWrap: 'wrap', marginTop: 12, }, benefitTag: { backgroundColor: colors.primaryLight, borderWidth: 1, borderColor: colors.primaryBorder, borderRadius: 12, paddingHorizontal: 10, paddingVertical: 4, marginRight: 8, marginBottom: 8, }, benefitText: { fontSize: 12, color: colors.primary, }, /* ---- 更多筛选面板 ---- */ modalOverlay: { flex: 1, backgroundColor: colors.scrim, justifyContent: 'flex-end', }, filterPanel: { backgroundColor: colors.white, borderTopLeftRadius: 20, borderTopRightRadius: 20, paddingHorizontal: 20, paddingTop: 16, paddingBottom: 24, }, panelHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4, }, panelTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary, }, filterLabel: { marginTop: 16, marginBottom: 10, fontSize: 14, fontWeight: '600', color: colors.textPrimary, }, optionRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, }, cityGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 12, marginTop: 16, }, optionChip: { paddingHorizontal: 14, paddingVertical: 8, borderRadius: 18, backgroundColor: colors.pageBg, borderWidth: 1, borderColor: colors.border, }, optionChipActive: { backgroundColor: colors.surface, borderColor: colors.borderStrong, }, optionText: { fontSize: 13, color: colors.textPrimary, }, optionTextActive: { color: colors.textPrimary, fontWeight: '700', }, panelActions: { flexDirection: 'row', gap: 12, marginTop: 24, }, resetBtn: { flex: 1, height: 44, borderRadius: 22, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.pageBg, borderWidth: 1, borderColor: colors.border, }, resetText: { fontSize: 15, fontWeight: '600', color: colors.textSecondary, }, confirmBtn: { flex: 1, height: 44, borderRadius: 22, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.primaryBtn, }, confirmText: { fontSize: 15, fontWeight: '600', color: colors.white, }, });