You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
528 lines
15 KiB
528 lines
15 KiB
import { Ionicons } from '@expo/vector-icons';
|
|
import { useRouter } from 'expo-router';
|
|
import {
|
|
ActivityIndicator,
|
|
Pressable,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
TextInput,
|
|
View,
|
|
} from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { useAppealForm } from '../src/hooks';
|
|
import { colors } 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 (
|
|
<View style={styles.header}>
|
|
<Pressable style={styles.headerBtn} onPress={onBack} hitSlop={8}>
|
|
<Ionicons name="chevron-back" size={24} color={colors.textPrimary} />
|
|
</Pressable>
|
|
<Text style={styles.headerTitle}>评分申诉</Text>
|
|
<View style={styles.headerBtn} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:申诉须知 ===================== */
|
|
|
|
function NoticeCard() {
|
|
return (
|
|
<View style={styles.noticeCard}>
|
|
<View style={styles.noticeHead}>
|
|
<Ionicons name="information-circle" size={18} color={colors.info} />
|
|
<Text style={styles.noticeTitle}>申诉须知</Text>
|
|
</View>
|
|
<Text style={styles.noticeText}>
|
|
1. 请在收到评价后 30 天内发起申诉,逾期将无法受理。
|
|
</Text>
|
|
<Text style={styles.noticeText}>
|
|
2. 请如实描述申诉原因,并提供相关凭证,虚假申诉将影响账号信用。
|
|
</Text>
|
|
<Text style={styles.noticeText}>
|
|
3. 平台将在 3 个工作日内完成审核,结果将通过站内消息通知你。
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:申诉类型 ===================== */
|
|
|
|
function TypeSection({
|
|
active,
|
|
onSelect,
|
|
}: {
|
|
active: AppealType;
|
|
onSelect: (t: AppealType) => void;
|
|
}) {
|
|
return (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>
|
|
申诉类型 <Text style={styles.required}>*</Text>
|
|
</Text>
|
|
{APPEAL_TYPES.map((item) => {
|
|
const selected = item.id === active;
|
|
return (
|
|
<Pressable
|
|
key={item.id}
|
|
style={[styles.typeCard, selected && styles.typeCardActive]}
|
|
onPress={() => onSelect(item.id)}
|
|
>
|
|
<View
|
|
style={[
|
|
styles.radioOuter,
|
|
selected && { borderColor: colors.primary, borderWidth: 5 },
|
|
]}
|
|
/>
|
|
<View style={styles.typeBody}>
|
|
<Text style={[styles.typeLabel, selected && styles.typeLabelActive]}>
|
|
{item.label}
|
|
</Text>
|
|
<Text style={styles.typeDesc}>{item.desc}</Text>
|
|
</View>
|
|
</Pressable>
|
|
);
|
|
})}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:申诉维度(多选) ===================== */
|
|
|
|
function DimensionSection({
|
|
dimensions,
|
|
active,
|
|
onToggle,
|
|
}: {
|
|
dimensions: { id: string; name: string }[];
|
|
active: string[];
|
|
onToggle: (id: string) => void;
|
|
}) {
|
|
return (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>
|
|
申请维度 <Text style={styles.optional}>(可多选)</Text>
|
|
</Text>
|
|
<View style={styles.dimWrap}>
|
|
{dimensions.map((dim) => {
|
|
const selected = active.includes(dim.id);
|
|
return (
|
|
<Pressable
|
|
key={dim.id}
|
|
style={[styles.dimTag, selected && styles.dimTagActive]}
|
|
onPress={() => onToggle(dim.id)}
|
|
>
|
|
{selected && (
|
|
<Ionicons name="checkmark" size={14} color={colors.white} />
|
|
)}
|
|
<Text style={[styles.dimTagText, selected && styles.dimTagTextActive]}>
|
|
{dim.name}
|
|
</Text>
|
|
</Pressable>
|
|
);
|
|
})}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:申诉原因 ===================== */
|
|
|
|
function ReasonSection({
|
|
active,
|
|
onSelect,
|
|
}: {
|
|
active: AppealReason | null;
|
|
onSelect: (r: AppealReason) => void;
|
|
}) {
|
|
return (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>
|
|
申诉原因 <Text style={styles.required}>*</Text>
|
|
</Text>
|
|
{APPEAL_REASONS.map((item) => {
|
|
const selected = item.id === active;
|
|
return (
|
|
<Pressable
|
|
key={item.id}
|
|
style={styles.reasonRow}
|
|
onPress={() => onSelect(item.id)}
|
|
>
|
|
<Ionicons
|
|
name={selected ? 'radio-button-on' : 'radio-button-off'}
|
|
size={20}
|
|
color={selected ? colors.primary : colors.borderStrong}
|
|
/>
|
|
<Text style={[styles.reasonLabel, selected && styles.reasonLabelActive]}>
|
|
{item.label}
|
|
</Text>
|
|
</Pressable>
|
|
);
|
|
})}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:详细说明 ===================== */
|
|
|
|
function DescriptionSection({
|
|
value,
|
|
onChange,
|
|
}: {
|
|
value: string;
|
|
onChange: (v: string) => void;
|
|
}) {
|
|
return (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>
|
|
详细说明 <Text style={styles.required}>*</Text>
|
|
</Text>
|
|
<TextInput
|
|
style={styles.textArea}
|
|
value={value}
|
|
onChangeText={onChange}
|
|
placeholder="请详细描述你的申诉原因,例如:评价中的项目经历与事实不符,实际参与了 XX 项目…"
|
|
placeholderTextColor={colors.textHint}
|
|
multiline
|
|
numberOfLines={5}
|
|
textAlignVertical="top"
|
|
/>
|
|
<Text style={styles.counter}>{value.length}/500</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:上传凭证 ===================== */
|
|
|
|
function EvidenceSection() {
|
|
return (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>上传凭证</Text>
|
|
<Pressable style={styles.uploadBox}>
|
|
<Ionicons name="cloud-upload-outline" size={28} color={colors.textHint} />
|
|
<Text style={styles.uploadText}>点击上传图片或文件</Text>
|
|
<Text style={styles.uploadHint}>支持 JPG / PNG / PDF,单个文件不超过 10MB</Text>
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:联系方式 ===================== */
|
|
|
|
function ContactSection({
|
|
phone,
|
|
email,
|
|
onPhoneChange,
|
|
onEmailChange,
|
|
}: {
|
|
phone: string;
|
|
email: string;
|
|
onPhoneChange: (v: string) => void;
|
|
onEmailChange: (v: string) => void;
|
|
}) {
|
|
return (
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>
|
|
联系方式 <Text style={styles.required}>*</Text>
|
|
</Text>
|
|
<View style={styles.card}>
|
|
<View style={styles.inputRow}>
|
|
<Text style={styles.inputLabel}>手机号</Text>
|
|
<TextInput
|
|
style={styles.input}
|
|
value={phone}
|
|
onChangeText={onPhoneChange}
|
|
placeholder="请输入手机号"
|
|
placeholderTextColor={colors.textHint}
|
|
keyboardType="phone-pad"
|
|
/>
|
|
</View>
|
|
<View style={styles.inputRow}>
|
|
<Text style={styles.inputLabel}>邮箱</Text>
|
|
<TextInput
|
|
style={styles.input}
|
|
value={email}
|
|
onChangeText={onEmailChange}
|
|
placeholder="请输入邮箱"
|
|
placeholderTextColor={colors.textHint}
|
|
keyboardType="email-address"
|
|
/>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:四态 ===================== */
|
|
|
|
function Loading() {
|
|
return (
|
|
<View style={styles.center}>
|
|
<ActivityIndicator size="large" color={colors.primary} />
|
|
<Text style={styles.tip}>正在加载申诉表单…</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function ErrorState({ onRetry }: { onRetry: () => void }) {
|
|
return (
|
|
<View style={styles.center}>
|
|
<Ionicons name="alert-circle-outline" size={40} color={colors.textHint} />
|
|
<Text style={styles.tip}>表单加载失败</Text>
|
|
<Pressable style={styles.retryBtn} onPress={onRetry} hitSlop={6}>
|
|
<Text style={styles.retryText}>点击重试</Text>
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 页面 ===================== */
|
|
|
|
export default function AppealScreen() {
|
|
const router = useRouter();
|
|
const {
|
|
form,
|
|
loading,
|
|
error,
|
|
refresh,
|
|
type,
|
|
setType,
|
|
dimensions,
|
|
toggleDimension,
|
|
reason,
|
|
setReason,
|
|
reasonText,
|
|
setReasonText,
|
|
contactPhone,
|
|
setContactPhone,
|
|
contactEmail,
|
|
setContactEmail,
|
|
submit,
|
|
submitting,
|
|
} = useAppealForm();
|
|
|
|
const handleSubmit = async () => {
|
|
const result = await submit();
|
|
if (result) {
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<Header onBack={() => router.back()} />
|
|
<Loading />
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
if (error || !form) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<Header onBack={() => router.back()} />
|
|
<ErrorState onRetry={refresh} />
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<Header onBack={() => router.back()} />
|
|
<ScrollView contentContainerStyle={styles.body} showsVerticalScrollIndicator={false}>
|
|
<NoticeCard />
|
|
<TypeSection active={type} onSelect={setType} />
|
|
<DimensionSection
|
|
dimensions={form.dimensions}
|
|
active={dimensions}
|
|
onToggle={toggleDimension}
|
|
/>
|
|
<ReasonSection active={reason} onSelect={setReason} />
|
|
<DescriptionSection value={reasonText} onChange={setReasonText} />
|
|
<EvidenceSection />
|
|
<ContactSection
|
|
phone={contactPhone}
|
|
email={contactEmail}
|
|
onPhoneChange={setContactPhone}
|
|
onEmailChange={setContactEmail}
|
|
/>
|
|
</ScrollView>
|
|
|
|
<View style={styles.footer}>
|
|
<Pressable
|
|
style={[styles.submitBtn, submitting && styles.submitBtnDisabled]}
|
|
onPress={handleSubmit}
|
|
disabled={submitting}
|
|
>
|
|
{submitting ? (
|
|
<ActivityIndicator size="small" color={colors.white} />
|
|
) : (
|
|
<Text style={styles.submitBtnText}>提交申诉</Text>
|
|
)}
|
|
</Pressable>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
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: 16, 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: 8,
|
|
borderRadius: 16,
|
|
backgroundColor: colors.primaryLight,
|
|
},
|
|
retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText },
|
|
noticeCard: {
|
|
backgroundColor: colors.infoLight,
|
|
borderRadius: 12,
|
|
padding: 14,
|
|
marginBottom: 16,
|
|
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: 12,
|
|
padding: 14,
|
|
marginBottom: 10,
|
|
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: 8,
|
|
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: 10,
|
|
paddingVertical: 12,
|
|
paddingHorizontal: 14,
|
|
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 },
|
|
card: {
|
|
backgroundColor: colors.white,
|
|
borderRadius: 10,
|
|
paddingHorizontal: 14,
|
|
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: 16,
|
|
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 },
|
|
});
|
|
|