Browse Source
隐私中心 8 子页: - app/settings/privacy/[id].tsx(动态路由):真实姓名可见性、联系方式、算法训练、第三方分析、位置授权、通讯录 6 个开关类二级页 - app/settings/privacy/dnd.tsx:勿扰模式(时段设置 + 重要通知穿透) - app/settings/privacy/keywords.tsx:关键词屏蔽(增删 + 系统建议) 合规 4 页(上架硬阻塞): - app/settings/account-cancel.tsx:账号注销(后果 + 待办 + 原因 + 二次确认) - app/settings/delete-data.tsx:删除数据(分类勾选 + 输入确认) - app/settings/privacy-policy.tsx / user-agreement.tsx:法律文档(共享 LegalDocScreen 渲染) 数据态:竞争力排名(app/jobs/rank.tsx)、岗位评价管理(app/job-evaluations.tsx)已展示数据态 所有页面沿用 theme token,无硬编码颜色;入口均已在隐私中心 mock 接入,无孤儿页面master
8 changed files with 1868 additions and 0 deletions
@ -0,0 +1,350 @@ |
|||
import { useState } from 'react'; |
|||
import { Ionicons } from '@expo/vector-icons'; |
|||
import { useRouter } from 'expo-router'; |
|||
import { |
|||
ActivityIndicator, |
|||
Pressable, |
|||
ScrollView, |
|||
StyleSheet, |
|||
Text, |
|||
View, |
|||
} from 'react-native'; |
|||
import { SafeAreaView } from 'react-native-safe-area-context'; |
|||
import { useAccountCancel } from '../../src/hooks'; |
|||
import { colors } from '../../src/theme'; |
|||
|
|||
/* ===================== 子组件:顶部导航 ===================== */ |
|||
|
|||
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> |
|||
); |
|||
} |
|||
|
|||
/* ===================== 页面 ===================== */ |
|||
|
|||
export default function AccountCancelScreen() { |
|||
const router = useRouter(); |
|||
const { data, loading, error, refresh, submit } = useAccountCancel(); |
|||
const [reason, setReason] = useState(''); |
|||
const [submitting, setSubmitting] = useState(false); |
|||
const [done, setDone] = useState<{ success: boolean; message: string } | null>(null); |
|||
|
|||
const handleSubmit = async () => { |
|||
setSubmitting(true); |
|||
try { |
|||
const res = await submit(reason); |
|||
setDone(res); |
|||
} finally { |
|||
setSubmitting(false); |
|||
} |
|||
}; |
|||
|
|||
if (loading) { |
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top']}> |
|||
<Header onBack={() => router.back()} /> |
|||
<View style={styles.stateBox}> |
|||
<ActivityIndicator size="large" color={colors.primary} /> |
|||
</View> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
if (error || !data) { |
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top']}> |
|||
<Header onBack={() => router.back()} /> |
|||
<View style={styles.stateBox}> |
|||
<Ionicons name="alert-circle-outline" size={40} color={colors.danger} /> |
|||
<Text style={styles.stateText}>加载失败,请重试</Text> |
|||
<Pressable style={styles.retryBtn} onPress={refresh}> |
|||
<Text style={styles.retryText}>重试</Text> |
|||
</Pressable> |
|||
</View> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
if (done?.success) { |
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top']}> |
|||
<Header onBack={() => router.back()} /> |
|||
<View style={styles.stateBox}> |
|||
<Ionicons name="checkmark-circle" size={48} color={colors.success} /> |
|||
<Text style={styles.stateText}>{done.message}</Text> |
|||
<Pressable style={styles.retryBtn} onPress={() => router.back()}> |
|||
<Text style={styles.retryText}>返回</Text> |
|||
</Pressable> |
|||
</View> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}> |
|||
<Header onBack={() => router.back()} /> |
|||
<ScrollView |
|||
style={styles.scroll} |
|||
contentContainerStyle={styles.content} |
|||
showsVerticalScrollIndicator={false} |
|||
> |
|||
<View style={[styles.warnCard, { backgroundColor: colors.dangerLight }]}> |
|||
<Ionicons name="warning-outline" size={18} color={colors.dangerText} /> |
|||
<Text style={[styles.warnText, { color: colors.dangerText }]}> |
|||
注销后账号将无法恢复,请谨慎操作 |
|||
</Text> |
|||
</View> |
|||
|
|||
<View style={styles.section}> |
|||
<Text style={styles.sectionTitle}>注销后将发生</Text> |
|||
<View style={styles.card}> |
|||
{data.consequences.map((item, index) => ( |
|||
<View |
|||
key={index} |
|||
style={[styles.bulletRow, index < data.consequences.length - 1 && styles.bulletBorder]} |
|||
> |
|||
<Ionicons name="close-circle-outline" size={18} color={colors.danger} /> |
|||
<Text style={styles.bulletText}>{item}</Text> |
|||
</View> |
|||
))} |
|||
</View> |
|||
</View> |
|||
|
|||
<View style={styles.section}> |
|||
<Text style={styles.sectionTitle}>注销前待办</Text> |
|||
<View style={styles.card}> |
|||
{data.checklist.map((item, index) => ( |
|||
<View |
|||
key={item.id} |
|||
style={[styles.checkRow, index < data.checklist.length - 1 && styles.rowBorder]} |
|||
> |
|||
<View style={styles.rowBody}> |
|||
<Text style={styles.rowTitle}>{item.label}</Text> |
|||
{item.actionText ? ( |
|||
<Pressable onPress={() => console.log('[account-cancel] 去处理', item.id)}> |
|||
<Text style={styles.actionText}>{item.actionText}</Text> |
|||
</Pressable> |
|||
) : null} |
|||
</View> |
|||
<Ionicons |
|||
name={item.resolved ? 'checkmark-circle' : 'ellipse-outline'} |
|||
size={20} |
|||
color={item.resolved ? colors.success : colors.textHint} |
|||
/> |
|||
</View> |
|||
))} |
|||
</View> |
|||
</View> |
|||
|
|||
<View style={styles.section}> |
|||
<Text style={styles.sectionTitle}>注销原因(选填)</Text> |
|||
<View style={styles.chipWrap}> |
|||
{data.reasons.map((r) => { |
|||
const active = reason === r; |
|||
return ( |
|||
<Pressable |
|||
key={r} |
|||
style={[styles.chip, active && styles.chipActive]} |
|||
onPress={() => setReason(active ? '' : r)} |
|||
> |
|||
<Text style={[styles.chipText, active && styles.chipTextActive]}>{r}</Text> |
|||
</Pressable> |
|||
); |
|||
})} |
|||
</View> |
|||
</View> |
|||
|
|||
<Pressable |
|||
style={[styles.submitBtn, (!data.canCancel || submitting) && styles.submitDisabled]} |
|||
disabled={!data.canCancel || submitting} |
|||
onPress={handleSubmit} |
|||
> |
|||
<Text style={styles.submitText}> |
|||
{submitting ? '提交中…' : data.canCancel ? '申请注销' : '请先完成注销前待办'} |
|||
</Text> |
|||
</Pressable> |
|||
</ScrollView> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
const styles = StyleSheet.create({ |
|||
safeArea: { |
|||
flex: 1, |
|||
backgroundColor: colors.pageBg, |
|||
}, |
|||
header: { |
|||
flexDirection: 'row', |
|||
alignItems: 'center', |
|||
justifyContent: 'space-between', |
|||
paddingHorizontal: 8, |
|||
height: 48, |
|||
backgroundColor: colors.white, |
|||
borderBottomWidth: 1, |
|||
borderBottomColor: colors.borderLight, |
|||
}, |
|||
headerBtn: { |
|||
width: 40, |
|||
height: 40, |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
}, |
|||
headerTitle: { |
|||
flex: 1, |
|||
textAlign: 'center', |
|||
fontSize: 17, |
|||
fontWeight: '700', |
|||
color: colors.textPrimary, |
|||
}, |
|||
scroll: { |
|||
flex: 1, |
|||
}, |
|||
content: { |
|||
padding: 12, |
|||
paddingBottom: 24, |
|||
}, |
|||
stateBox: { |
|||
flex: 1, |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
gap: 12, |
|||
}, |
|||
stateText: { |
|||
marginTop: 12, |
|||
fontSize: 14, |
|||
color: colors.textSecondary, |
|||
paddingHorizontal: 32, |
|||
textAlign: 'center', |
|||
}, |
|||
retryBtn: { |
|||
marginTop: 16, |
|||
paddingHorizontal: 24, |
|||
paddingVertical: 8, |
|||
borderRadius: 18, |
|||
backgroundColor: colors.primaryBtn, |
|||
}, |
|||
retryText: { |
|||
fontSize: 13, |
|||
fontWeight: '600', |
|||
color: colors.white, |
|||
}, |
|||
warnCard: { |
|||
flexDirection: 'row', |
|||
alignItems: 'center', |
|||
gap: 8, |
|||
borderRadius: 12, |
|||
padding: 14, |
|||
marginBottom: 16, |
|||
}, |
|||
warnText: { |
|||
flex: 1, |
|||
fontSize: 13, |
|||
fontWeight: '600', |
|||
lineHeight: 18, |
|||
}, |
|||
section: { |
|||
marginBottom: 16, |
|||
}, |
|||
sectionTitle: { |
|||
fontSize: 14, |
|||
fontWeight: '700', |
|||
color: colors.textPrimary, |
|||
marginBottom: 10, |
|||
}, |
|||
card: { |
|||
backgroundColor: colors.white, |
|||
borderRadius: 12, |
|||
overflow: 'hidden', |
|||
}, |
|||
bulletRow: { |
|||
flexDirection: 'row', |
|||
alignItems: 'flex-start', |
|||
paddingHorizontal: 14, |
|||
paddingVertical: 12, |
|||
gap: 8, |
|||
}, |
|||
bulletBorder: { |
|||
borderBottomWidth: 1, |
|||
borderBottomColor: colors.borderLight, |
|||
}, |
|||
bulletText: { |
|||
flex: 1, |
|||
fontSize: 13, |
|||
lineHeight: 19, |
|||
color: colors.textSecondary, |
|||
}, |
|||
checkRow: { |
|||
flexDirection: 'row', |
|||
alignItems: 'center', |
|||
justifyContent: 'space-between', |
|||
paddingHorizontal: 14, |
|||
paddingVertical: 14, |
|||
}, |
|||
rowBorder: { |
|||
borderBottomWidth: 1, |
|||
borderBottomColor: colors.borderLight, |
|||
}, |
|||
rowBody: { |
|||
flex: 1, |
|||
marginRight: 12, |
|||
}, |
|||
rowTitle: { |
|||
fontSize: 15, |
|||
fontWeight: '600', |
|||
color: colors.textPrimary, |
|||
}, |
|||
actionText: { |
|||
marginTop: 3, |
|||
fontSize: 12, |
|||
fontWeight: '600', |
|||
color: colors.primary, |
|||
}, |
|||
chipWrap: { |
|||
flexDirection: 'row', |
|||
flexWrap: 'wrap', |
|||
gap: 8, |
|||
}, |
|||
chip: { |
|||
paddingVertical: 7, |
|||
paddingHorizontal: 14, |
|||
borderRadius: 16, |
|||
backgroundColor: colors.white, |
|||
borderWidth: 1, |
|||
borderColor: colors.border, |
|||
}, |
|||
chipActive: { |
|||
backgroundColor: colors.primaryLight, |
|||
borderColor: colors.primary, |
|||
}, |
|||
chipText: { |
|||
fontSize: 13, |
|||
fontWeight: '600', |
|||
color: colors.textSecondary, |
|||
}, |
|||
chipTextActive: { |
|||
color: colors.primary, |
|||
}, |
|||
submitBtn: { |
|||
marginTop: 8, |
|||
height: 48, |
|||
borderRadius: 24, |
|||
backgroundColor: colors.danger, |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
}, |
|||
submitDisabled: { |
|||
backgroundColor: colors.border, |
|||
}, |
|||
submitText: { |
|||
fontSize: 15, |
|||
fontWeight: '700', |
|||
color: colors.white, |
|||
}, |
|||
}); |
|||
@ -0,0 +1,373 @@ |
|||
import { useState } from 'react'; |
|||
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 { useDeleteAllData } from '../../src/hooks'; |
|||
import { colors } from '../../src/theme'; |
|||
|
|||
/* ===================== 子组件:顶部导航 ===================== */ |
|||
|
|||
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 CheckRow({ |
|||
title, |
|||
desc, |
|||
count, |
|||
checked, |
|||
onPress, |
|||
}: { |
|||
title: string; |
|||
desc?: string; |
|||
count?: string; |
|||
checked: boolean; |
|||
onPress: () => void; |
|||
}) { |
|||
return ( |
|||
<Pressable style={styles.row} onPress={onPress}> |
|||
<View style={styles.rowBody}> |
|||
<Text style={styles.rowTitle}>{title}</Text> |
|||
{desc ? <Text style={styles.rowDesc}>{desc}</Text> : null} |
|||
</View> |
|||
<View style={[styles.checkbox, checked && styles.checkboxActive]}> |
|||
{checked ? <Ionicons name="checkmark" size={14} color={colors.white} /> : null} |
|||
</View> |
|||
{count ? <Text style={styles.rowCount}>{count}</Text> : null} |
|||
</Pressable> |
|||
); |
|||
} |
|||
|
|||
/* ===================== 页面 ===================== */ |
|||
|
|||
export default function DeleteAllDataScreen() { |
|||
const router = useRouter(); |
|||
const { data, loading, error, refresh, submit } = useDeleteAllData(); |
|||
const [selected, setSelected] = useState<Set<string>>(new Set()); |
|||
const [confirmText, setConfirmText] = useState(''); |
|||
const [submitting, setSubmitting] = useState(false); |
|||
const [done, setDone] = useState<{ success: boolean; message: string } | null>(null); |
|||
|
|||
const toggleCat = (id: string) => { |
|||
setSelected((prev) => { |
|||
const next = new Set(prev); |
|||
if (next.has(id)) next.delete(id); |
|||
else next.add(id); |
|||
return next; |
|||
}); |
|||
}; |
|||
|
|||
const handleSubmit = async () => { |
|||
setSubmitting(true); |
|||
try { |
|||
const res = await submit(Array.from(selected)); |
|||
setDone(res); |
|||
} finally { |
|||
setSubmitting(false); |
|||
} |
|||
}; |
|||
|
|||
if (loading) { |
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top']}> |
|||
<Header onBack={() => router.back()} /> |
|||
<View style={styles.stateBox}> |
|||
<ActivityIndicator size="large" color={colors.primary} /> |
|||
</View> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
if (error || !data) { |
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top']}> |
|||
<Header onBack={() => router.back()} /> |
|||
<View style={styles.stateBox}> |
|||
<Ionicons name="alert-circle-outline" size={40} color={colors.danger} /> |
|||
<Text style={styles.stateText}>加载失败,请重试</Text> |
|||
<Pressable style={styles.retryBtn} onPress={refresh}> |
|||
<Text style={styles.retryText}>重试</Text> |
|||
</Pressable> |
|||
</View> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
if (done?.success) { |
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top']}> |
|||
<Header onBack={() => router.back()} /> |
|||
<View style={styles.stateBox}> |
|||
<Ionicons name="checkmark-circle" size={48} color={colors.success} /> |
|||
<Text style={styles.stateText}>{done.message}</Text> |
|||
<Pressable style={styles.retryBtn} onPress={() => router.back()}> |
|||
<Text style={styles.retryText}>返回</Text> |
|||
</Pressable> |
|||
</View> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
const canSubmit = selected.size > 0 && confirmText === '删除全部数据' && !submitting; |
|||
|
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}> |
|||
<Header onBack={() => router.back()} /> |
|||
<ScrollView |
|||
style={styles.scroll} |
|||
contentContainerStyle={styles.content} |
|||
showsVerticalScrollIndicator={false} |
|||
> |
|||
<View style={styles.introCard}> |
|||
<Text style={styles.introText}>{data.intro}</Text> |
|||
</View> |
|||
|
|||
<View style={styles.section}> |
|||
<Text style={styles.sectionTitle}>选择要删除的数据</Text> |
|||
<View style={styles.card}> |
|||
{data.categories.map((cat) => ( |
|||
<CheckRow |
|||
key={cat.id} |
|||
title={cat.label} |
|||
desc={cat.description} |
|||
count={cat.count} |
|||
checked={selected.has(cat.id)} |
|||
onPress={() => toggleCat(cat.id)} |
|||
/> |
|||
))} |
|||
</View> |
|||
</View> |
|||
|
|||
<View style={[styles.warnCard, { backgroundColor: colors.dangerLight }]}> |
|||
<Ionicons name="warning-outline" size={18} color={colors.dangerText} /> |
|||
<Text style={[styles.warnText, { color: colors.dangerText }]}>{data.warning}</Text> |
|||
</View> |
|||
|
|||
<View style={styles.card}> |
|||
<Text style={styles.confirmTitle}>确认删除</Text> |
|||
<Text style={styles.confirmHint}> |
|||
请输入「{data.confirmPlaceholder.replace('请输入「', '').replace('」以确认', '')}」以确认操作 |
|||
</Text> |
|||
<TextInput |
|||
style={styles.confirmInput} |
|||
value={confirmText} |
|||
onChangeText={setConfirmText} |
|||
placeholder={data.confirmPlaceholder} |
|||
placeholderTextColor={colors.textHint} |
|||
/> |
|||
</View> |
|||
|
|||
<Pressable |
|||
style={[styles.submitBtn, !canSubmit && styles.submitDisabled]} |
|||
disabled={!canSubmit} |
|||
onPress={handleSubmit} |
|||
> |
|||
<Text style={styles.submitText}> |
|||
{submitting ? '提交中…' : `删除选中数据(${selected.size} 项)`} |
|||
</Text> |
|||
</Pressable> |
|||
</ScrollView> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
const styles = StyleSheet.create({ |
|||
safeArea: { |
|||
flex: 1, |
|||
backgroundColor: colors.pageBg, |
|||
}, |
|||
header: { |
|||
flexDirection: 'row', |
|||
alignItems: 'center', |
|||
justifyContent: 'space-between', |
|||
paddingHorizontal: 8, |
|||
height: 48, |
|||
backgroundColor: colors.white, |
|||
borderBottomWidth: 1, |
|||
borderBottomColor: colors.borderLight, |
|||
}, |
|||
headerBtn: { |
|||
width: 40, |
|||
height: 40, |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
}, |
|||
headerTitle: { |
|||
flex: 1, |
|||
textAlign: 'center', |
|||
fontSize: 17, |
|||
fontWeight: '700', |
|||
color: colors.textPrimary, |
|||
}, |
|||
scroll: { |
|||
flex: 1, |
|||
}, |
|||
content: { |
|||
padding: 12, |
|||
paddingBottom: 24, |
|||
}, |
|||
stateBox: { |
|||
flex: 1, |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
gap: 12, |
|||
}, |
|||
stateText: { |
|||
marginTop: 12, |
|||
fontSize: 14, |
|||
color: colors.textSecondary, |
|||
paddingHorizontal: 32, |
|||
textAlign: 'center', |
|||
}, |
|||
retryBtn: { |
|||
marginTop: 16, |
|||
paddingHorizontal: 24, |
|||
paddingVertical: 8, |
|||
borderRadius: 18, |
|||
backgroundColor: colors.primaryBtn, |
|||
}, |
|||
retryText: { |
|||
fontSize: 13, |
|||
fontWeight: '600', |
|||
color: colors.white, |
|||
}, |
|||
introCard: { |
|||
backgroundColor: colors.infoLight, |
|||
borderRadius: 12, |
|||
padding: 14, |
|||
marginBottom: 16, |
|||
}, |
|||
introText: { |
|||
fontSize: 13, |
|||
lineHeight: 19, |
|||
color: colors.textSecondary, |
|||
}, |
|||
section: { |
|||
marginBottom: 16, |
|||
}, |
|||
sectionTitle: { |
|||
fontSize: 14, |
|||
fontWeight: '700', |
|||
color: colors.textPrimary, |
|||
marginBottom: 10, |
|||
}, |
|||
card: { |
|||
backgroundColor: colors.white, |
|||
borderRadius: 12, |
|||
overflow: 'hidden', |
|||
padding: 4, |
|||
}, |
|||
row: { |
|||
flexDirection: 'row', |
|||
alignItems: 'center', |
|||
paddingHorizontal: 14, |
|||
paddingVertical: 14, |
|||
gap: 12, |
|||
}, |
|||
rowBody: { |
|||
flex: 1, |
|||
}, |
|||
rowTitle: { |
|||
fontSize: 15, |
|||
fontWeight: '600', |
|||
color: colors.textPrimary, |
|||
}, |
|||
rowDesc: { |
|||
marginTop: 3, |
|||
fontSize: 12, |
|||
lineHeight: 16, |
|||
color: colors.textSecondary, |
|||
}, |
|||
rowCount: { |
|||
fontSize: 12, |
|||
color: colors.textHint, |
|||
marginLeft: 4, |
|||
}, |
|||
checkbox: { |
|||
width: 22, |
|||
height: 22, |
|||
borderRadius: 6, |
|||
borderWidth: 1.5, |
|||
borderColor: colors.border, |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
}, |
|||
checkboxActive: { |
|||
backgroundColor: colors.primary, |
|||
borderColor: colors.primary, |
|||
}, |
|||
warnCard: { |
|||
flexDirection: 'row', |
|||
alignItems: 'flex-start', |
|||
gap: 8, |
|||
borderRadius: 12, |
|||
padding: 14, |
|||
marginBottom: 16, |
|||
}, |
|||
warnText: { |
|||
flex: 1, |
|||
fontSize: 13, |
|||
fontWeight: '600', |
|||
lineHeight: 18, |
|||
}, |
|||
confirmTitle: { |
|||
fontSize: 15, |
|||
fontWeight: '700', |
|||
color: colors.textPrimary, |
|||
paddingHorizontal: 14, |
|||
paddingTop: 12, |
|||
}, |
|||
confirmHint: { |
|||
fontSize: 12, |
|||
color: colors.textSecondary, |
|||
paddingHorizontal: 14, |
|||
marginTop: 4, |
|||
marginBottom: 10, |
|||
}, |
|||
confirmInput: { |
|||
marginHorizontal: 14, |
|||
marginBottom: 14, |
|||
height: 42, |
|||
borderRadius: 10, |
|||
borderWidth: 1, |
|||
borderColor: colors.border, |
|||
paddingHorizontal: 12, |
|||
fontSize: 14, |
|||
color: colors.textPrimary, |
|||
}, |
|||
submitBtn: { |
|||
marginTop: 8, |
|||
height: 48, |
|||
borderRadius: 24, |
|||
backgroundColor: colors.danger, |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
}, |
|||
submitDisabled: { |
|||
backgroundColor: colors.border, |
|||
}, |
|||
submitText: { |
|||
fontSize: 15, |
|||
fontWeight: '700', |
|||
color: colors.white, |
|||
}, |
|||
}); |
|||
@ -0,0 +1,5 @@ |
|||
import { LegalDocScreen } from '../../src/components/LegalDocScreen'; |
|||
|
|||
export default function PrivacyPolicyScreen() { |
|||
return <LegalDocScreen docId="privacy-policy" fallbackTitle="隐私政策" />; |
|||
} |
|||
@ -0,0 +1,279 @@ |
|||
import { Ionicons } from '@expo/vector-icons'; |
|||
import { useLocalSearchParams, useRouter } from 'expo-router'; |
|||
import { |
|||
ActivityIndicator, |
|||
Pressable, |
|||
ScrollView, |
|||
StyleSheet, |
|||
Switch, |
|||
Text, |
|||
View, |
|||
} from 'react-native'; |
|||
import { SafeAreaView } from 'react-native-safe-area-context'; |
|||
import { usePrivacyDetailSetting } from '../../../src/hooks'; |
|||
import { colors } from '../../../src/theme'; |
|||
import type { PrivacyDetailOption } from '../../../src/types'; |
|||
|
|||
/* ===================== 子组件:顶部导航 ===================== */ |
|||
|
|||
function Header({ title, onBack }: { title: string; 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} numberOfLines={1}> |
|||
{title} |
|||
</Text> |
|||
<View style={styles.headerBtn} /> |
|||
</View> |
|||
); |
|||
} |
|||
|
|||
/* ===================== 子组件:可选项行 ===================== */ |
|||
|
|||
function OptionRow({ |
|||
item, |
|||
disabled, |
|||
isLast, |
|||
onToggle, |
|||
}: { |
|||
item: PrivacyDetailOption; |
|||
disabled: boolean; |
|||
isLast: boolean; |
|||
onToggle: (id: string, enabled: boolean) => void; |
|||
}) { |
|||
return ( |
|||
<View style={[styles.row, !isLast && styles.rowBorder]}> |
|||
<View style={styles.rowBody}> |
|||
<Text style={[styles.rowTitle, disabled && styles.rowTitleDisabled]}>{item.title}</Text> |
|||
{item.description ? <Text style={styles.rowDesc}>{item.description}</Text> : null} |
|||
</View> |
|||
<Switch |
|||
value={item.enabled} |
|||
disabled={disabled} |
|||
onValueChange={(next) => onToggle(item.id, next)} |
|||
trackColor={{ false: colors.border, true: colors.primaryBorder }} |
|||
thumbColor={item.enabled ? colors.primary : colors.white} |
|||
/> |
|||
</View> |
|||
); |
|||
} |
|||
|
|||
/* ===================== 页面 ===================== */ |
|||
|
|||
export default function PrivacyDetailScreen() { |
|||
const params = useLocalSearchParams(); |
|||
const rawId = params.id; |
|||
const id = typeof rawId === 'string' ? rawId : Array.isArray(rawId) ? rawId[0] ?? '' : ''; |
|||
const router = useRouter(); |
|||
const { detail, loading, error, refresh, toggle } = usePrivacyDetailSetting(id); |
|||
|
|||
if (loading) { |
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top']}> |
|||
<Header title="隐私设置" onBack={() => router.back()} /> |
|||
<View style={styles.stateBox}> |
|||
<ActivityIndicator size="large" color={colors.primary} /> |
|||
</View> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
if (error || !detail) { |
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top']}> |
|||
<Header title="隐私设置" onBack={() => router.back()} /> |
|||
<View style={styles.stateBox}> |
|||
<Ionicons name="alert-circle-outline" size={40} color={colors.danger} /> |
|||
<Text style={styles.stateText}>{detail === null ? '暂无该设置项' : '加载失败,请重试'}</Text> |
|||
<Pressable style={styles.retryBtn} onPress={refresh}> |
|||
<Text style={styles.retryText}>重试</Text> |
|||
</Pressable> |
|||
</View> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}> |
|||
<Header title={detail.title} onBack={() => router.back()} /> |
|||
<ScrollView |
|||
style={styles.scroll} |
|||
contentContainerStyle={styles.content} |
|||
showsVerticalScrollIndicator={false} |
|||
> |
|||
<View style={styles.introCard}> |
|||
<Text style={styles.introText}>{detail.intro}</Text> |
|||
</View> |
|||
|
|||
<View style={styles.section}> |
|||
<Text style={styles.sectionTitle}>主开关</Text> |
|||
<View style={styles.card}> |
|||
<View style={styles.row}> |
|||
<View style={styles.rowBody}> |
|||
<Text style={styles.rowTitle}>{detail.master.title}</Text> |
|||
</View> |
|||
<Switch |
|||
value={detail.master.enabled} |
|||
onValueChange={(next) => toggle(detail.master.id, next)} |
|||
trackColor={{ false: colors.border, true: colors.primaryBorder }} |
|||
thumbColor={detail.master.enabled ? colors.primary : colors.white} |
|||
/> |
|||
</View> |
|||
</View> |
|||
</View> |
|||
|
|||
<View style={styles.section}> |
|||
<Text style={styles.sectionTitle}>细选项</Text> |
|||
<View style={styles.card}> |
|||
{detail.options.map((item, index) => ( |
|||
<OptionRow |
|||
key={item.id} |
|||
item={item} |
|||
disabled={!detail.master.enabled} |
|||
isLast={index === detail.options.length - 1} |
|||
onToggle={toggle} |
|||
/> |
|||
))} |
|||
</View> |
|||
</View> |
|||
|
|||
{detail.note ? ( |
|||
<View style={styles.noteCard}> |
|||
<Ionicons name="information-circle-outline" size={18} color={colors.info} /> |
|||
<Text style={styles.noteText}>{detail.note}</Text> |
|||
</View> |
|||
) : null} |
|||
</ScrollView> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
const styles = StyleSheet.create({ |
|||
safeArea: { |
|||
flex: 1, |
|||
backgroundColor: colors.pageBg, |
|||
}, |
|||
header: { |
|||
flexDirection: 'row', |
|||
alignItems: 'center', |
|||
justifyContent: 'space-between', |
|||
paddingHorizontal: 8, |
|||
height: 48, |
|||
backgroundColor: colors.white, |
|||
borderBottomWidth: 1, |
|||
borderBottomColor: colors.borderLight, |
|||
}, |
|||
headerBtn: { |
|||
width: 40, |
|||
height: 40, |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
}, |
|||
headerTitle: { |
|||
flex: 1, |
|||
textAlign: 'center', |
|||
fontSize: 17, |
|||
fontWeight: '700', |
|||
color: colors.textPrimary, |
|||
}, |
|||
scroll: { |
|||
flex: 1, |
|||
}, |
|||
content: { |
|||
padding: 12, |
|||
paddingBottom: 24, |
|||
}, |
|||
stateBox: { |
|||
flex: 1, |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
gap: 12, |
|||
}, |
|||
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, |
|||
}, |
|||
introCard: { |
|||
backgroundColor: colors.infoLight, |
|||
borderRadius: 12, |
|||
padding: 14, |
|||
marginBottom: 16, |
|||
}, |
|||
introText: { |
|||
fontSize: 13, |
|||
lineHeight: 19, |
|||
color: colors.textSecondary, |
|||
}, |
|||
section: { |
|||
marginBottom: 16, |
|||
}, |
|||
sectionTitle: { |
|||
fontSize: 14, |
|||
fontWeight: '700', |
|||
color: colors.textPrimary, |
|||
marginBottom: 10, |
|||
}, |
|||
card: { |
|||
backgroundColor: colors.white, |
|||
borderRadius: 12, |
|||
overflow: 'hidden', |
|||
}, |
|||
row: { |
|||
flexDirection: 'row', |
|||
alignItems: 'center', |
|||
justifyContent: 'space-between', |
|||
paddingHorizontal: 14, |
|||
paddingVertical: 14, |
|||
}, |
|||
rowBorder: { |
|||
borderBottomWidth: 1, |
|||
borderBottomColor: colors.borderLight, |
|||
}, |
|||
rowBody: { |
|||
flex: 1, |
|||
marginRight: 12, |
|||
}, |
|||
rowTitle: { |
|||
fontSize: 15, |
|||
fontWeight: '600', |
|||
color: colors.textPrimary, |
|||
}, |
|||
rowTitleDisabled: { |
|||
color: colors.textHint, |
|||
}, |
|||
rowDesc: { |
|||
marginTop: 3, |
|||
fontSize: 12, |
|||
lineHeight: 16, |
|||
color: colors.textSecondary, |
|||
}, |
|||
noteCard: { |
|||
flexDirection: 'row', |
|||
alignItems: 'flex-start', |
|||
backgroundColor: colors.white, |
|||
borderRadius: 12, |
|||
padding: 14, |
|||
gap: 8, |
|||
}, |
|||
noteText: { |
|||
flex: 1, |
|||
fontSize: 12, |
|||
lineHeight: 18, |
|||
color: colors.textSecondary, |
|||
}, |
|||
}); |
|||
@ -0,0 +1,295 @@ |
|||
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 { useDndSetting } from '../../../src/hooks'; |
|||
import { colors } from '../../../src/theme'; |
|||
|
|||
/* ===================== 子组件:顶部导航 ===================== */ |
|||
|
|||
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 TimeRow({ |
|||
label, |
|||
value, |
|||
onChange, |
|||
}: { |
|||
label: string; |
|||
value: string; |
|||
onChange: (text: string) => void; |
|||
}) { |
|||
return ( |
|||
<View style={styles.row}> |
|||
<Text style={styles.rowTitle}>{label}</Text> |
|||
<TextInput |
|||
style={styles.timeInput} |
|||
value={value} |
|||
onChangeText={onChange} |
|||
placeholder="00:00" |
|||
placeholderTextColor={colors.textHint} |
|||
keyboardType="numbers-and-punctuation" |
|||
/> |
|||
</View> |
|||
); |
|||
} |
|||
|
|||
/* ===================== 页面 ===================== */ |
|||
|
|||
export default function DndSettingScreen() { |
|||
const router = useRouter(); |
|||
const { setting, loading, error, refresh, update } = useDndSetting(); |
|||
|
|||
if (loading) { |
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top']}> |
|||
<Header onBack={() => router.back()} /> |
|||
<View style={styles.stateBox}> |
|||
<ActivityIndicator size="large" color={colors.primary} /> |
|||
</View> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
if (error || !setting) { |
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top']}> |
|||
<Header onBack={() => router.back()} /> |
|||
<View style={styles.stateBox}> |
|||
<Ionicons name="alert-circle-outline" size={40} color={colors.danger} /> |
|||
<Text style={styles.stateText}>加载失败,请重试</Text> |
|||
<Pressable style={styles.retryBtn} onPress={refresh}> |
|||
<Text style={styles.retryText}>重试</Text> |
|||
</Pressable> |
|||
</View> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}> |
|||
<Header onBack={() => router.back()} /> |
|||
<ScrollView |
|||
style={styles.scroll} |
|||
contentContainerStyle={styles.content} |
|||
showsVerticalScrollIndicator={false} |
|||
> |
|||
<View style={[styles.statCard, { backgroundColor: colors.primaryLight }]}> |
|||
<Text style={[styles.statValue, { color: colors.primary }]}>{setting.blockedCount}</Text> |
|||
<Text style={styles.statLabel}>累计拦截的通知条数</Text> |
|||
</View> |
|||
|
|||
<View style={styles.section}> |
|||
<Text style={styles.sectionTitle}>基础设置</Text> |
|||
<View style={styles.card}> |
|||
<View style={[styles.row, styles.rowBorder]}> |
|||
<View style={styles.rowBody}> |
|||
<Text style={styles.rowTitle}>开启勿扰模式</Text> |
|||
</View> |
|||
<Switch |
|||
value={setting.enabled} |
|||
onValueChange={(next) => update({ enabled: next })} |
|||
trackColor={{ false: colors.border, true: colors.primaryBorder }} |
|||
thumbColor={setting.enabled ? colors.primary : colors.white} |
|||
/> |
|||
</View> |
|||
<TimeRow |
|||
label="开始时间" |
|||
value={setting.startTime} |
|||
onChange={(text) => update({ startTime: text })} |
|||
/> |
|||
<View style={styles.rowBorder} /> |
|||
<TimeRow |
|||
label="结束时间" |
|||
value={setting.endTime} |
|||
onChange={(text) => update({ endTime: text })} |
|||
/> |
|||
</View> |
|||
</View> |
|||
|
|||
<View style={styles.section}> |
|||
<Text style={styles.sectionTitle}>高级设置</Text> |
|||
<View style={styles.card}> |
|||
<View style={styles.row}> |
|||
<View style={styles.rowBody}> |
|||
<Text style={styles.rowTitle}>允许重要通知穿透</Text> |
|||
<Text style={styles.rowDesc}>面试邀约等与企业的重要沟通仍会提醒你</Text> |
|||
</View> |
|||
<Switch |
|||
value={setting.allowUrgent} |
|||
onValueChange={(next) => update({ allowUrgent: next })} |
|||
trackColor={{ false: colors.border, true: colors.primaryBorder }} |
|||
thumbColor={setting.allowUrgent ? colors.primary : colors.white} |
|||
/> |
|||
</View> |
|||
</View> |
|||
</View> |
|||
|
|||
<View style={styles.noteCard}> |
|||
<Ionicons name="information-circle-outline" size={18} color={colors.info} /> |
|||
<Text style={styles.noteText}> |
|||
开启勿扰模式后,{setting.startTime} 至 {setting.endTime} 期间的非重要通知将被静音。你仍可在「企业沟通」中查看错过的消息。 |
|||
</Text> |
|||
</View> |
|||
</ScrollView> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
const styles = StyleSheet.create({ |
|||
safeArea: { |
|||
flex: 1, |
|||
backgroundColor: colors.pageBg, |
|||
}, |
|||
header: { |
|||
flexDirection: 'row', |
|||
alignItems: 'center', |
|||
justifyContent: 'space-between', |
|||
paddingHorizontal: 8, |
|||
height: 48, |
|||
backgroundColor: colors.white, |
|||
borderBottomWidth: 1, |
|||
borderBottomColor: colors.borderLight, |
|||
}, |
|||
headerBtn: { |
|||
width: 40, |
|||
height: 40, |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
}, |
|||
headerTitle: { |
|||
flex: 1, |
|||
textAlign: 'center', |
|||
fontSize: 17, |
|||
fontWeight: '700', |
|||
color: colors.textPrimary, |
|||
}, |
|||
scroll: { |
|||
flex: 1, |
|||
}, |
|||
content: { |
|||
padding: 12, |
|||
paddingBottom: 24, |
|||
}, |
|||
stateBox: { |
|||
flex: 1, |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
gap: 12, |
|||
}, |
|||
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, |
|||
}, |
|||
statCard: { |
|||
borderRadius: 12, |
|||
padding: 20, |
|||
alignItems: 'center', |
|||
marginBottom: 16, |
|||
}, |
|||
statValue: { |
|||
fontSize: 32, |
|||
fontWeight: '800', |
|||
}, |
|||
statLabel: { |
|||
marginTop: 4, |
|||
fontSize: 13, |
|||
color: colors.textSecondary, |
|||
}, |
|||
section: { |
|||
marginBottom: 16, |
|||
}, |
|||
sectionTitle: { |
|||
fontSize: 14, |
|||
fontWeight: '700', |
|||
color: colors.textPrimary, |
|||
marginBottom: 10, |
|||
}, |
|||
card: { |
|||
backgroundColor: colors.white, |
|||
borderRadius: 12, |
|||
overflow: 'hidden', |
|||
}, |
|||
row: { |
|||
flexDirection: 'row', |
|||
alignItems: 'center', |
|||
justifyContent: 'space-between', |
|||
paddingHorizontal: 14, |
|||
paddingVertical: 14, |
|||
}, |
|||
rowBorder: { |
|||
borderBottomWidth: 1, |
|||
borderBottomColor: colors.borderLight, |
|||
}, |
|||
rowBody: { |
|||
flex: 1, |
|||
marginRight: 12, |
|||
}, |
|||
rowTitle: { |
|||
fontSize: 15, |
|||
fontWeight: '600', |
|||
color: colors.textPrimary, |
|||
}, |
|||
rowDesc: { |
|||
marginTop: 3, |
|||
fontSize: 12, |
|||
lineHeight: 16, |
|||
color: colors.textSecondary, |
|||
}, |
|||
timeInput: { |
|||
minWidth: 64, |
|||
textAlign: 'right', |
|||
fontSize: 15, |
|||
fontWeight: '600', |
|||
color: colors.primary, |
|||
paddingVertical: 4, |
|||
paddingHorizontal: 8, |
|||
}, |
|||
noteCard: { |
|||
flexDirection: 'row', |
|||
alignItems: 'flex-start', |
|||
backgroundColor: colors.white, |
|||
borderRadius: 12, |
|||
padding: 14, |
|||
gap: 8, |
|||
}, |
|||
noteText: { |
|||
flex: 1, |
|||
fontSize: 12, |
|||
lineHeight: 18, |
|||
color: colors.textSecondary, |
|||
}, |
|||
}); |
|||
@ -0,0 +1,357 @@ |
|||
import { useState } from 'react'; |
|||
import { Ionicons } from '@expo/vector-icons'; |
|||
import { useRouter } from 'expo-router'; |
|||
import { |
|||
ActivityIndicator, |
|||
KeyboardAvoidingView, |
|||
Platform, |
|||
Pressable, |
|||
ScrollView, |
|||
StyleSheet, |
|||
Text, |
|||
TextInput, |
|||
View, |
|||
} from 'react-native'; |
|||
import { SafeAreaView } from 'react-native-safe-area-context'; |
|||
import { useKeywordBlock } from '../../../src/hooks'; |
|||
import { colors } from '../../../src/theme'; |
|||
|
|||
/* ===================== 子组件:顶部导航 ===================== */ |
|||
|
|||
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 KeywordChip({ |
|||
text, |
|||
onRemove, |
|||
}: { |
|||
text: string; |
|||
onRemove: () => void; |
|||
}) { |
|||
return ( |
|||
<View style={styles.chip}> |
|||
<Text style={styles.chipText}>{text}</Text> |
|||
<Pressable hitSlop={6} onPress={onRemove}> |
|||
<Ionicons name="close-circle" size={16} color={colors.textHint} /> |
|||
</Pressable> |
|||
</View> |
|||
); |
|||
} |
|||
|
|||
/* ===================== 页面 ===================== */ |
|||
|
|||
export default function KeywordBlockScreen() { |
|||
const router = useRouter(); |
|||
const { setting, loading, error, refresh, addKeyword, removeKeyword } = useKeywordBlock(); |
|||
const [text, setText] = useState(''); |
|||
|
|||
const handleAdd = async () => { |
|||
const value = text.trim(); |
|||
if (!value) return; |
|||
await addKeyword(value); |
|||
setText(''); |
|||
}; |
|||
|
|||
if (loading) { |
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top']}> |
|||
<Header onBack={() => router.back()} /> |
|||
<View style={styles.stateBox}> |
|||
<ActivityIndicator size="large" color={colors.primary} /> |
|||
</View> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
if (error || !setting) { |
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top']}> |
|||
<Header onBack={() => router.back()} /> |
|||
<View style={styles.stateBox}> |
|||
<Ionicons name="alert-circle-outline" size={40} color={colors.danger} /> |
|||
<Text style={styles.stateText}>加载失败,请重试</Text> |
|||
<Pressable style={styles.retryBtn} onPress={refresh}> |
|||
<Text style={styles.retryText}>重试</Text> |
|||
</Pressable> |
|||
</View> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}> |
|||
<Header onBack={() => router.back()} /> |
|||
<KeyboardAvoidingView |
|||
style={styles.flex} |
|||
behavior={Platform.OS === 'ios' ? 'padding' : undefined} |
|||
> |
|||
<ScrollView |
|||
style={styles.scroll} |
|||
contentContainerStyle={styles.content} |
|||
showsVerticalScrollIndicator={false} |
|||
> |
|||
<View style={[styles.statCard, { backgroundColor: colors.dangerLight }]}> |
|||
<Text style={[styles.statValue, { color: colors.dangerText }]}>{setting.blockedCount}</Text> |
|||
<Text style={styles.statLabel}>累计拦截的招聘信息条数</Text> |
|||
</View> |
|||
|
|||
<View style={styles.card}> |
|||
<Text style={styles.cardTitle}>已屏蔽关键词</Text> |
|||
{setting.keywords.length === 0 ? ( |
|||
<Text style={styles.emptyText}>暂无屏蔽词,添加后相关岗位与消息将被过滤</Text> |
|||
) : ( |
|||
<View style={styles.chipWrap}> |
|||
{setting.keywords.map((word) => ( |
|||
<KeywordChip |
|||
key={word} |
|||
text={word} |
|||
onRemove={() => removeKeyword(word)} |
|||
/> |
|||
))} |
|||
</View> |
|||
)} |
|||
</View> |
|||
|
|||
{setting.suggestions.length > 0 ? ( |
|||
<View style={styles.card}> |
|||
<Text style={styles.cardTitle}>系统建议屏蔽</Text> |
|||
<Text style={styles.cardHint}>点击即可快速添加高频骚扰词</Text> |
|||
<View style={styles.chipWrap}> |
|||
{setting.suggestions.map((word) => ( |
|||
<Pressable |
|||
key={word} |
|||
style={styles.suggestChip} |
|||
onPress={() => addKeyword(word)} |
|||
> |
|||
<Ionicons name="add" size={14} color={colors.primary} /> |
|||
<Text style={styles.suggestText}>{word}</Text> |
|||
</Pressable> |
|||
))} |
|||
</View> |
|||
</View> |
|||
) : null} |
|||
|
|||
<View style={styles.noteCard}> |
|||
<Ionicons name="information-circle-outline" size={18} color={colors.info} /> |
|||
<Text style={styles.noteText}> |
|||
屏蔽关键词后,标题或详情包含该词的职位推荐与沟通消息将被隐藏,不影响你已投递的岗位。 |
|||
</Text> |
|||
</View> |
|||
</ScrollView> |
|||
|
|||
<View style={styles.inputBar}> |
|||
<TextInput |
|||
style={styles.input} |
|||
value={text} |
|||
onChangeText={setText} |
|||
placeholder="输入要屏蔽的关键词" |
|||
placeholderTextColor={colors.textHint} |
|||
returnKeyType="done" |
|||
onSubmitEditing={handleAdd} |
|||
/> |
|||
<Pressable |
|||
style={[styles.addBtn, !text.trim() && styles.addBtnDisabled]} |
|||
disabled={!text.trim()} |
|||
onPress={handleAdd} |
|||
> |
|||
<Text style={styles.addBtnText}>添加</Text> |
|||
</Pressable> |
|||
</View> |
|||
</KeyboardAvoidingView> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
const styles = StyleSheet.create({ |
|||
safeArea: { |
|||
flex: 1, |
|||
backgroundColor: colors.pageBg, |
|||
}, |
|||
flex: { |
|||
flex: 1, |
|||
}, |
|||
header: { |
|||
flexDirection: 'row', |
|||
alignItems: 'center', |
|||
justifyContent: 'space-between', |
|||
paddingHorizontal: 8, |
|||
height: 48, |
|||
backgroundColor: colors.white, |
|||
borderBottomWidth: 1, |
|||
borderBottomColor: colors.borderLight, |
|||
}, |
|||
headerBtn: { |
|||
width: 40, |
|||
height: 40, |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
}, |
|||
headerTitle: { |
|||
flex: 1, |
|||
textAlign: 'center', |
|||
fontSize: 17, |
|||
fontWeight: '700', |
|||
color: colors.textPrimary, |
|||
}, |
|||
scroll: { |
|||
flex: 1, |
|||
}, |
|||
content: { |
|||
padding: 12, |
|||
paddingBottom: 16, |
|||
}, |
|||
stateBox: { |
|||
flex: 1, |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
gap: 12, |
|||
}, |
|||
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, |
|||
}, |
|||
statCard: { |
|||
borderRadius: 12, |
|||
padding: 20, |
|||
alignItems: 'center', |
|||
marginBottom: 16, |
|||
}, |
|||
statValue: { |
|||
fontSize: 32, |
|||
fontWeight: '800', |
|||
}, |
|||
statLabel: { |
|||
marginTop: 4, |
|||
fontSize: 13, |
|||
color: colors.textSecondary, |
|||
}, |
|||
card: { |
|||
backgroundColor: colors.white, |
|||
borderRadius: 12, |
|||
padding: 16, |
|||
marginBottom: 16, |
|||
}, |
|||
cardTitle: { |
|||
fontSize: 15, |
|||
fontWeight: '700', |
|||
color: colors.textPrimary, |
|||
marginBottom: 4, |
|||
}, |
|||
cardHint: { |
|||
fontSize: 12, |
|||
color: colors.textSecondary, |
|||
marginBottom: 12, |
|||
}, |
|||
emptyText: { |
|||
fontSize: 13, |
|||
lineHeight: 19, |
|||
color: colors.textSecondary, |
|||
}, |
|||
chipWrap: { |
|||
flexDirection: 'row', |
|||
flexWrap: 'wrap', |
|||
gap: 8, |
|||
marginTop: 12, |
|||
}, |
|||
chip: { |
|||
flexDirection: 'row', |
|||
alignItems: 'center', |
|||
backgroundColor: colors.dangerLight, |
|||
borderRadius: 16, |
|||
paddingVertical: 6, |
|||
paddingHorizontal: 10, |
|||
gap: 4, |
|||
}, |
|||
chipText: { |
|||
fontSize: 13, |
|||
fontWeight: '600', |
|||
color: colors.dangerText, |
|||
}, |
|||
suggestChip: { |
|||
flexDirection: 'row', |
|||
alignItems: 'center', |
|||
backgroundColor: colors.primaryLight, |
|||
borderRadius: 16, |
|||
paddingVertical: 6, |
|||
paddingHorizontal: 10, |
|||
gap: 2, |
|||
}, |
|||
suggestText: { |
|||
fontSize: 13, |
|||
fontWeight: '600', |
|||
color: colors.primary, |
|||
}, |
|||
noteCard: { |
|||
flexDirection: 'row', |
|||
alignItems: 'flex-start', |
|||
backgroundColor: colors.white, |
|||
borderRadius: 12, |
|||
padding: 14, |
|||
gap: 8, |
|||
}, |
|||
noteText: { |
|||
flex: 1, |
|||
fontSize: 12, |
|||
lineHeight: 18, |
|||
color: colors.textSecondary, |
|||
}, |
|||
inputBar: { |
|||
flexDirection: 'row', |
|||
alignItems: 'center', |
|||
gap: 8, |
|||
paddingHorizontal: 12, |
|||
paddingVertical: 10, |
|||
backgroundColor: colors.white, |
|||
borderTopWidth: 1, |
|||
borderTopColor: colors.borderLight, |
|||
}, |
|||
input: { |
|||
flex: 1, |
|||
height: 40, |
|||
borderRadius: 20, |
|||
backgroundColor: colors.pageBg, |
|||
paddingHorizontal: 16, |
|||
fontSize: 14, |
|||
color: colors.textPrimary, |
|||
}, |
|||
addBtn: { |
|||
height: 40, |
|||
paddingHorizontal: 20, |
|||
borderRadius: 20, |
|||
backgroundColor: colors.primaryBtn, |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
}, |
|||
addBtnDisabled: { |
|||
backgroundColor: colors.border, |
|||
}, |
|||
addBtnText: { |
|||
fontSize: 14, |
|||
fontWeight: '700', |
|||
color: colors.white, |
|||
}, |
|||
}); |
|||
@ -0,0 +1,5 @@ |
|||
import { LegalDocScreen } from '../../src/components/LegalDocScreen'; |
|||
|
|||
export default function UserAgreementScreen() { |
|||
return <LegalDocScreen docId="user-agreement" fallbackTitle="用户协议" />; |
|||
} |
|||
@ -0,0 +1,204 @@ |
|||
import { Ionicons } from '@expo/vector-icons'; |
|||
import { useRouter } from 'expo-router'; |
|||
import { |
|||
ActivityIndicator, |
|||
Pressable, |
|||
ScrollView, |
|||
StyleSheet, |
|||
Text, |
|||
View, |
|||
} from 'react-native'; |
|||
import { SafeAreaView } from 'react-native-safe-area-context'; |
|||
import { useLegalDocument } from '../hooks'; |
|||
import { colors } from '../theme'; |
|||
|
|||
/* ===================== 子组件:顶部导航 ===================== */ |
|||
|
|||
function Header({ title, onBack }: { title: string; 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} numberOfLines={1}> |
|||
{title} |
|||
</Text> |
|||
<View style={styles.headerBtn} /> |
|||
</View> |
|||
); |
|||
} |
|||
|
|||
/* ===================== 共享页面:法律文档(隐私政策 / 用户协议) ===================== */ |
|||
|
|||
export function LegalDocScreen({ |
|||
docId, |
|||
fallbackTitle, |
|||
}: { |
|||
docId: string; |
|||
fallbackTitle: string; |
|||
}) { |
|||
const router = useRouter(); |
|||
const { doc, loading, error, refresh } = useLegalDocument(docId); |
|||
|
|||
if (loading) { |
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top']}> |
|||
<Header title={fallbackTitle} onBack={() => router.back()} /> |
|||
<View style={styles.stateBox}> |
|||
<ActivityIndicator size="large" color={colors.primary} /> |
|||
</View> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
if (error || !doc) { |
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top']}> |
|||
<Header title={fallbackTitle} onBack={() => router.back()} /> |
|||
<View style={styles.stateBox}> |
|||
<Ionicons name="alert-circle-outline" size={40} color={colors.danger} /> |
|||
<Text style={styles.stateText}>加载失败,请重试</Text> |
|||
<Pressable style={styles.retryBtn} onPress={refresh}> |
|||
<Text style={styles.retryText}>重试</Text> |
|||
</Pressable> |
|||
</View> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
return ( |
|||
<SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}> |
|||
<Header title={doc.title} onBack={() => router.back()} /> |
|||
<ScrollView |
|||
style={styles.scroll} |
|||
contentContainerStyle={styles.content} |
|||
showsVerticalScrollIndicator={false} |
|||
> |
|||
<View style={styles.metaRow}> |
|||
<Text style={styles.metaText}>版本 {doc.version}</Text> |
|||
<Text style={styles.metaText}>更新于 {doc.updatedAt}</Text> |
|||
</View> |
|||
|
|||
<View style={styles.preambleCard}> |
|||
<Text style={styles.preambleText}>{doc.preamble}</Text> |
|||
</View> |
|||
|
|||
{doc.sections.map((section, index) => ( |
|||
<View key={index} style={styles.section}> |
|||
<Text style={styles.sectionHeading}>{section.heading}</Text> |
|||
{section.paragraphs.map((p, pIndex) => ( |
|||
<Text key={pIndex} style={styles.paragraph}> |
|||
{p} |
|||
</Text> |
|||
))} |
|||
</View> |
|||
))} |
|||
|
|||
<Text style={styles.footNote}> |
|||
本文件含【待确认】占位项,正式发布前需由法务复核并替换公司全称、联系方式、管辖法院等信息。 |
|||
</Text> |
|||
</ScrollView> |
|||
</SafeAreaView> |
|||
); |
|||
} |
|||
|
|||
const styles = StyleSheet.create({ |
|||
safeArea: { |
|||
flex: 1, |
|||
backgroundColor: colors.pageBg, |
|||
}, |
|||
header: { |
|||
flexDirection: 'row', |
|||
alignItems: 'center', |
|||
justifyContent: 'space-between', |
|||
paddingHorizontal: 8, |
|||
height: 48, |
|||
backgroundColor: colors.white, |
|||
borderBottomWidth: 1, |
|||
borderBottomColor: colors.borderLight, |
|||
}, |
|||
headerBtn: { |
|||
width: 40, |
|||
height: 40, |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
}, |
|||
headerTitle: { |
|||
flex: 1, |
|||
textAlign: 'center', |
|||
fontSize: 17, |
|||
fontWeight: '700', |
|||
color: colors.textPrimary, |
|||
}, |
|||
scroll: { |
|||
flex: 1, |
|||
}, |
|||
content: { |
|||
padding: 16, |
|||
paddingBottom: 32, |
|||
}, |
|||
stateBox: { |
|||
flex: 1, |
|||
alignItems: 'center', |
|||
justifyContent: 'center', |
|||
gap: 12, |
|||
}, |
|||
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, |
|||
}, |
|||
metaRow: { |
|||
flexDirection: 'row', |
|||
justifyContent: 'space-between', |
|||
marginBottom: 12, |
|||
}, |
|||
metaText: { |
|||
fontSize: 12, |
|||
color: colors.textHint, |
|||
}, |
|||
preambleCard: { |
|||
backgroundColor: colors.infoLight, |
|||
borderRadius: 12, |
|||
padding: 14, |
|||
marginBottom: 16, |
|||
}, |
|||
preambleText: { |
|||
fontSize: 13, |
|||
lineHeight: 20, |
|||
color: colors.textSecondary, |
|||
}, |
|||
section: { |
|||
marginBottom: 16, |
|||
}, |
|||
sectionHeading: { |
|||
fontSize: 15, |
|||
fontWeight: '700', |
|||
color: colors.textPrimary, |
|||
marginBottom: 8, |
|||
}, |
|||
paragraph: { |
|||
fontSize: 13, |
|||
lineHeight: 21, |
|||
color: colors.textSecondary, |
|||
marginBottom: 8, |
|||
}, |
|||
footNote: { |
|||
fontSize: 11, |
|||
lineHeight: 16, |
|||
color: colors.textHint, |
|||
marginTop: 8, |
|||
}, |
|||
}); |
|||
Loading…
Reference in new issue