import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { ActivityIndicator, Alert, Image, Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useAppealForm, useImagePick } from '../src/hooks'; import {colors, spacing} from '../src/theme'; import type { AppealReason, AppealType } from '../src/types'; /** 申诉类型选项 */ const APPEAL_TYPES: { id: AppealType; label: string; desc: string }[] = [ { id: 'overall', label: '综合评分申诉', desc: '对整体评分结果有异议' }, { id: 'single', label: '单条评价申诉', desc: '对某一条 HR 评价有异议' }, ]; /** 申诉原因选项 */ const APPEAL_REASONS: { id: AppealReason; label: string }[] = [ { id: 'untrue', label: '评分与事实不符' }, { id: 'malicious', label: '评价存在主观偏见' }, { id: 'privacy', label: '涉及个人隐私信息' }, { id: 'other', label: '其他原因' }, ]; /* ===================== 子组件:顶部导航 ===================== */ function Header({ onBack }: { onBack: () => void }) { return ( 评分申诉 ); } /* ===================== 子组件:申诉须知 ===================== */ function NoticeCard() { return ( 申诉须知 1. 请在收到评价后 30 天内发起申诉,逾期将无法受理。 2. 请如实描述申诉原因,并提供相关凭证,虚假申诉将影响账号信用。 3. 平台将在 3 个工作日内完成审核,结果将通过站内消息通知你。 ); } /* ===================== 子组件:申诉类型 ===================== */ function TypeSection({ active, onSelect, }: { active: AppealType; onSelect: (t: AppealType) => void; }) { return ( 申诉类型 * {APPEAL_TYPES.map((item) => { const selected = item.id === active; return ( onSelect(item.id)} > {item.label} {item.desc} ); })} ); } /* ===================== 子组件:申诉维度(多选) ===================== */ function DimensionSection({ dimensions, active, onToggle, }: { dimensions: { id: string; name: string }[]; active: string[]; onToggle: (id: string) => void; }) { return ( 申请维度 (可多选) {dimensions.map((dim) => { const selected = active.includes(dim.id); return ( onToggle(dim.id)} > {selected && ( )} {dim.name} ); })} ); } /* ===================== 子组件:申诉原因 ===================== */ function ReasonSection({ active, onSelect, }: { active: AppealReason | null; onSelect: (r: AppealReason) => void; }) { return ( 申诉原因 * {APPEAL_REASONS.map((item) => { const selected = item.id === active; return ( onSelect(item.id)} > {item.label} ); })} ); } /* ===================== 子组件:详细说明 ===================== */ function DescriptionSection({ value, onChange, }: { value: string; onChange: (v: string) => void; }) { return ( 详细说明 * {value.length}/500 ); } /* ===================== 子组件:上传凭证 ===================== */ function EvidenceSection({ evidence, picking, error, onPick, onRemove, }: { evidence: string[]; picking: boolean; error: string | null; onPick: (source: 'camera' | 'library') => void; onRemove: (uri: string) => void; }) { const chooseSource = () => { Alert.alert('上传凭证', '选择图片来源', [ { text: '拍照', onPress: () => onPick('camera') }, { text: '从相册选择', onPress: () => onPick('library') }, { text: '取消', style: 'cancel' }, ]); }; return ( 上传凭证({evidence.length}) {evidence.length > 0 && ( {evidence.map((uri) => ( onRemove(uri)} hitSlop={6}> ))} )} {picking ? ( ) : ( )} {picking ? '正在打开相册…' : '点击上传图片'} 当前支持 JPG / PNG 图片,单张不超过 10MB;文件(PDF)待上传接口就绪后开放 {error ? {error} : null} ); } /* ===================== 子组件:联系方式 ===================== */ function ContactSection({ phone, email, onPhoneChange, onEmailChange, }: { phone: string; email: string; onPhoneChange: (v: string) => void; onEmailChange: (v: string) => void; }) { return ( 联系方式 * 手机号 邮箱 ); } /* ===================== 子组件:四态 ===================== */ function Loading() { return ( 正在加载申诉表单… ); } function ErrorState({ onRetry }: { onRetry: () => void }) { return ( 表单加载失败 点击重试 ); } /* ===================== 页面 ===================== */ export default function AppealScreen() { const router = useRouter(); const { form, loading, error, refresh, type, setType, dimensions, toggleDimension, reason, setReason, reasonText, setReasonText, evidence, addEvidence, removeEvidence, contactPhone, setContactPhone, contactEmail, setContactEmail, submit, submitting, } = useAppealForm(); const { pick, picking, error: pickError } = useImagePick(); const handleSubmit = async () => { const result = await submit(); if (result) { Alert.alert('提交成功', '我们会在 3 个工作日内完成审核,结果将通过站内消息通知你。', [ { text: '确定', onPress: () => router.back() }, ]); } }; /** 选一张证据图:取消与未授权由 useImagePick 区分(未授权给出设置指引) */ const handlePickEvidence = async (source: 'camera' | 'library') => { const result = await pick(source, { allowsEditing: false }); if (result.status === 'picked') addEvidence(result.uri); }; if (loading) { return (
router.back()} /> ); } if (error || !form) { return (
router.back()} /> ); } return (
router.back()} /> {submitting ? ( ) : ( 提交申诉 )} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.pageBg }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 12, paddingVertical: 12, backgroundColor: colors.white, }, headerBtn: { width: 40, alignItems: 'center' }, headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary }, body: { paddingHorizontal: spacing.card, paddingBottom: 100, paddingTop: 12 }, center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 40, gap: 12 }, tip: { fontSize: 14, color: colors.textSecondary }, retryBtn: { paddingHorizontal: 18, paddingVertical: spacing.card, borderRadius: 16, backgroundColor: colors.primaryLight, }, retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText }, noticeCard: { backgroundColor: colors.infoLight, borderRadius: spacing.card, padding: spacing.card, marginBottom: 8, borderWidth: 1, borderColor: colors.infoBorder, }, noticeHead: { flexDirection: 'row', alignItems: 'center', marginBottom: 8 }, noticeTitle: { fontSize: 14, fontWeight: '700', color: colors.infoText, marginLeft: 6 }, noticeText: { fontSize: 12, color: colors.infoText, lineHeight: 20, marginBottom: 2 }, section: { marginBottom: 16 }, sectionTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, marginBottom: 10 }, required: { color: colors.danger }, optional: { fontSize: 12, fontWeight: '400', color: colors.textHint }, typeCard: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.white, borderRadius: spacing.card, padding: spacing.card, marginBottom: 8, borderWidth: 1, borderColor: colors.borderLight, }, typeCardActive: { borderColor: colors.primary, backgroundColor: colors.primaryLight }, radioOuter: { width: 18, height: 18, borderRadius: 9, borderWidth: 1, borderColor: colors.borderStrong, }, typeBody: { flex: 1, marginLeft: 12 }, typeLabel: { fontSize: 14, fontWeight: '700', color: colors.textPrimary }, typeLabelActive: { color: colors.primaryText }, typeDesc: { fontSize: 12, color: colors.textSecondary, marginTop: 2 }, dimWrap: { flexDirection: 'row', flexWrap: 'wrap', gap: 10 }, dimTag: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: spacing.card, borderRadius: 18, backgroundColor: colors.white, borderWidth: 1, borderColor: colors.border, }, dimTagActive: { backgroundColor: colors.primary, borderColor: colors.primary }, dimTagText: { fontSize: 13, color: colors.textSecondary, marginLeft: 4 }, dimTagTextActive: { color: colors.white, fontWeight: '600' }, reasonRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.white, borderRadius: spacing.card, paddingVertical: spacing.card, paddingHorizontal: spacing.card, marginBottom: 8, borderWidth: 1, borderColor: colors.borderLight, }, reasonLabel: { fontSize: 14, color: colors.textPrimary, marginLeft: 10 }, reasonLabelActive: { color: colors.primaryText, fontWeight: '600' }, textArea: { height: 120, backgroundColor: colors.white, borderRadius: 10, borderWidth: 1, borderColor: colors.borderLight, padding: 12, fontSize: 14, color: colors.textPrimary, }, counter: { marginTop: 6, fontSize: 12, color: colors.textHint, textAlign: 'right' }, uploadBox: { backgroundColor: colors.white, borderRadius: 10, borderWidth: 1, borderColor: colors.borderLight, borderStyle: 'dashed', paddingVertical: 24, alignItems: 'center', }, uploadText: { fontSize: 14, color: colors.textPrimary, marginTop: 8 }, uploadHint: { fontSize: 11, color: colors.textHint, marginTop: 4 }, uploadBoxDisabled: { opacity: 0.6 }, uploadError: { fontSize: 12, color: colors.dangerText, marginTop: 8 }, evidenceRow: { marginBottom: 10 }, evidenceItem: { marginRight: 10 }, evidenceImage: { width: 72, height: 72, borderRadius: spacing.card, backgroundColor: colors.pageBg, }, evidenceRemove: { position: 'absolute', top: -6, right: -6, width: 20, height: 20, borderRadius: 10, backgroundColor: colors.textPrimary, alignItems: 'center', justifyContent: 'center', }, card: { backgroundColor: colors.white, borderRadius: spacing.card, paddingHorizontal: spacing.card, borderWidth: 1, borderColor: colors.borderLight, }, inputRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: colors.borderLight, }, inputLabel: { width: 60, fontSize: 14, color: colors.textPrimary }, input: { flex: 1, fontSize: 14, color: colors.textPrimary }, footer: { position: 'absolute', left: 0, right: 0, bottom: 0, paddingHorizontal: spacing.framework, paddingVertical: 12, backgroundColor: colors.white, borderTopWidth: 1, borderTopColor: colors.borderLight, }, submitBtn: { backgroundColor: colors.primaryBtn, borderRadius: 24, paddingVertical: 14, alignItems: 'center', }, submitBtnDisabled: { backgroundColor: colors.textDisabled }, submitBtnText: { fontSize: 16, fontWeight: '700', color: colors.white }, });