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-3年 / 3-5年 / 5-10年 / 10年以上),
* 与 `JobExperienceRequirement` 的成员逐一对应。原先发布端多出的「1年以内」属悬空值
* (筛选侧无对应档位、mock 数据也未使用),已按产品口径移除。
*/
const EXPERIENCE_OPTIONS: JobPublishFormData['experience'][] = [
'应届生',
'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: spacing.card,
paddingBottom: 100,
},
card: {
backgroundColor: colors.white,
borderRadius: spacing.card,
padding: spacing.card,
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: spacing.card,
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,
},
});