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