import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Switch, Text, TextInput, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useJobPublish } from '../src/hooks'; import { LoadingState, ErrorState } from '../src/components/StateView'; import {colors, spacing} from '../src/theme'; import { EDUCATION_OPTIONS, WORK_TYPE_LABELS } from '../src/constants'; import type { JobPublishFormData } from '../src/types'; /** 工作类型选项(标签与编码的对照表来自 constants,此处只是换个形状消费) */ const WORK_TYPE_OPTIONS = WORK_TYPE_LABELS; /** * 经验要求选项 —— **刻意不从 constants 的 `EXPERIENCE_OPTIONS` 取**。 * * 两张表不是同一个事实,名字像而已: * · constants 的 `EXPERIENCE_OPTIONS` 是**筛选项**,含「不限」(表示不加限制), * 且不含「1年以内」; * · 本表是**企业发布岗位时的必填要求**,没有「不限」(必须明确要求), * 且含「1年以内」—— 与 `JobExperienceRequirement` 的成员逐一对应。 * 强行合并会让发布页出现「不限」这个不该有的选项。 * * 已知遗留(非本批处理):`JobExperienceRequirement` 的「1年以内」在 mock 岗位数据里 * **一次都没用到**,而筛选项表里也没有它 —— 即这个取值目前既筛不到、也没有样本。 * 属数据/类型层面的取舍,需产品定夺,已记入《项目开发状态.md》4.16。 */ const EXPERIENCE_OPTIONS: JobPublishFormData['experience'][] = [ '应届生', '1年以内', '1-3年', '3-5年', '5-10年', '10年以上', ]; const BENEFIT_OPTIONS = ['五险一金', '年终奖金', '股票期权', '带薪年假', '弹性工作', '餐补', '交通补助', '定期体检']; const SKILL_OPTIONS = ['React', 'Vue', 'TypeScript', 'Node.js', 'Webpack', 'Git', 'Python', 'Java', 'Go', 'SQL']; /* ===================== 子组件:顶部导航 ===================== */ function Header({ onBack, onSave }: { onBack: () => void; onSave: () => void }) { return ( 发布岗位 保存 ); } /* ===================== 子组件:表单项 ===================== */ function Field({ label, required, children }: { label: string; required?: boolean; children: React.ReactNode }) { return ( {label} {required && *} {children} ); } function Input({ value, onChangeText, placeholder, multiline }: { value: string; onChangeText: (text: string) => void; placeholder?: string; multiline?: boolean; }) { return ( ); } function NumberInput({ value, onChangeText, placeholder }: { value: number; onChangeText: (text: string) => void; placeholder?: string; }) { return ( ); } /* ===================== 子组件:单选标签组 ===================== */ function RadioGroup({ options, value, onChange, }: { /** 只读:选项表现在有一份来自 constants 的共享常量(`WORK_TYPE_LABELS`),不该被就地改写 */ options: readonly T[] | ReadonlyArray<{ label: string; value: T }>; value: T; onChange: (value: T) => void; }) { return ( {options.map((opt) => { const optionValue = typeof opt === 'string' ? opt : opt.value; const optionLabel = typeof opt === 'string' ? opt : opt.label; const selected = optionValue === value; return ( onChange(optionValue)} > {optionLabel} ); })} ); } /* ===================== 子组件:多选标签组 ===================== */ function CheckboxGroup({ options, values, onToggle }: { options: string[]; values: string[]; onToggle: (v: string) => void }) { return ( {options.map((opt) => { const selected = values.includes(opt); return ( onToggle(opt)} > {selected && } {opt} ); })} ); } /* ===================== 子组件:开关行 ===================== */ function SwitchRow({ label, value, onValueChange }: { label: string; value: boolean; onValueChange: (v: boolean) => void }) { return ( {label} ); } /* ===================== 子组件:质量预估卡 ===================== */ function QualityEstimateCard({ score, level, suggestions, estimating }: { score: number; level: string; suggestions: string[]; estimating: boolean; }) { return ( 岗位质量预估 AI 实时评估岗位信息完整度 {estimating ? ( ) : ( <> {score} {level} )} {suggestions.length > 0 && ( {suggestions.map((s, i) => ( {s} ))} )} ); } /* ===================== 页面主组件 ===================== */ export default function JobPublishScreen() { const router = useRouter(); const { form, loading, error, updateStringField, updateNumberField, updateField, toggleBenefit, toggleSkill, estimate, estimating, runEstimate, publishing, publish, refresh, } = useJobPublish(); if (loading) { return ( ); } if (error || !form) { return ( ); } return (
router.back()} onSave={() => {}} /> {/* 基本信息 */} 基本信息 updateStringField('positionName', v)} placeholder="请输入岗位名称" /> updateStringField('department', v)} placeholder="请输入所属部门" /> updateNumberField('headcount', v)} placeholder="请输入招聘人数" /> updateStringField('city', v)} placeholder="请输入工作城市" /> updateStringField('address', v)} placeholder="请输入详细地址" /> options={WORK_TYPE_OPTIONS} value={form.workType} onChange={(v) => updateField('workType', v)} /> {/* 岗位要求 */} 岗位要求 options={EDUCATION_OPTIONS} value={form.education} onChange={(v) => updateField('education', v)} /> options={EXPERIENCE_OPTIONS} value={form.experience} onChange={(v) => updateField('experience', v)} /> {/* 薪资福利 */} 薪资福利 updateNumberField('salaryMin', v)} placeholder="最低" /> - updateNumberField('salaryMax', v)} placeholder="最高" /> updateNumberField('salaryMonths', v)} placeholder="如 14" /> {/* 岗位描述 */} 岗位描述 updateStringField('responsibilities', v)} placeholder="请输入岗位职责" multiline /> updateStringField('requirements', v)} placeholder="请输入岗位要求" multiline /> {/* 岗位设置 */} 岗位设置 options={[ { label: '全部可见', value: 'all' as const }, { label: '内部推荐', value: 'internal' as const }, { label: '校园招聘', value: 'campus' as const }, ]} value={form.visibility} onChange={(v) => updateField('visibility', v)} /> updateField('urgent', v)} /> updateField('acceptFresh', v)} /> updateStringField('deadline', v)} placeholder="YYYY-MM-DD" /> {/* 质量预估 */} 质量预估 {publishing ? '发布中...' : '发布岗位'} ); } /* ===================== 样式 ===================== */ const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.pageBg, }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: colors.pageBg, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: spacing.framework, paddingVertical: 12, backgroundColor: colors.white, borderBottomWidth: 1, borderBottomColor: colors.borderLight, }, headerBtn: { width: 36, height: 36, justifyContent: 'center', alignItems: 'center', }, headerTitle: { fontSize: 17, fontWeight: '600', color: colors.textPrimary, }, headerSaveBtn: { paddingHorizontal: 12, paddingVertical: 6, backgroundColor: colors.primaryLight, borderRadius: 16, }, headerSaveText: { fontSize: 13, fontWeight: '600', color: colors.primary, }, scroll: { flex: 1, }, scrollContent: { padding: 8, paddingBottom: 100, }, card: { backgroundColor: colors.white, borderRadius: 8, padding: 8, marginBottom: 8, }, cardTitle: { fontSize: 16, fontWeight: '700', color: colors.textPrimary, marginBottom: 16, }, field: { marginBottom: 16, }, fieldLabel: { fontSize: 14, fontWeight: '500', color: colors.textPrimary, marginBottom: 8, }, required: { color: colors.danger, }, input: { height: 44, borderWidth: 1, borderColor: colors.border, borderRadius: 10, paddingHorizontal: 12, fontSize: 14, color: colors.textPrimary, backgroundColor: colors.pageBg, }, textarea: { height: 100, paddingTop: 12, paddingBottom: 12, }, tagRow: { flexDirection: 'row', flexWrap: 'wrap', marginHorizontal: -4, }, radioTag: { paddingHorizontal: 14, paddingVertical: 8, borderRadius: 18, borderWidth: 1, borderColor: colors.border, margin: 4, backgroundColor: colors.white, }, radioTagActive: { backgroundColor: colors.primary, borderColor: colors.primary, }, radioText: { fontSize: 13, color: colors.textPrimary, }, radioTextActive: { color: colors.white, fontWeight: '500', }, checkboxTag: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, paddingVertical: 7, borderRadius: 18, borderWidth: 1, borderColor: colors.border, margin: 4, backgroundColor: colors.white, }, checkboxTagActive: { backgroundColor: colors.primary, borderColor: colors.primary, }, checkboxText: { fontSize: 13, color: colors.textPrimary, marginLeft: 4, }, checkboxTextActive: { color: colors.white, fontWeight: '500', }, switchRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: colors.borderLight, }, switchLabel: { fontSize: 14, color: colors.textPrimary, }, salaryRow: { flexDirection: 'row', alignItems: 'flex-end', }, salarySeparator: { marginHorizontal: 8, marginBottom: 16, fontSize: 16, color: colors.textSecondary, }, qualityCard: { backgroundColor: colors.primaryLight, borderWidth: 1, borderColor: colors.primaryBorder, }, qualityHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, qualitySub: { fontSize: 12, color: colors.textSecondary, marginTop: 4, }, qualityScoreBox: { alignItems: 'center', }, qualityScore: { fontSize: 32, fontWeight: '700', color: colors.primary, }, qualityLevel: { fontSize: 13, color: colors.primaryText, fontWeight: '500', }, suggestionList: { marginTop: 16, paddingTop: 16, borderTopWidth: 1, borderTopColor: colors.primaryBorder, }, suggestionItem: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 8, }, suggestionText: { flex: 1, fontSize: 13, color: colors.textSecondary, marginLeft: 8, lineHeight: 18, }, footer: { flexDirection: 'row', padding: spacing.framework, paddingBottom: 24, backgroundColor: colors.white, borderTopWidth: 1, borderTopColor: colors.borderLight, }, draftBtn: { flex: 1, height: 46, borderRadius: 24, borderWidth: 1, borderColor: colors.primary, justifyContent: 'center', alignItems: 'center', marginRight: 12, }, draftText: { fontSize: 15, fontWeight: '600', color: colors.primary, }, publishBtn: { flex: 2, height: 46, borderRadius: 24, backgroundColor: colors.primaryBtn, justifyContent: 'center', alignItems: 'center', }, publishBtnDisabled: { opacity: 0.6, }, publishText: { fontSize: 15, fontWeight: '600', color: colors.white, }, });