From 78de95135f8ee94f7a4ce1bc7f05b4379b030a00 Mon Sep 17 00:00:00 2001 From: doublex001 <16157008+doublex001@user.noreply.gitee.com> Date: Mon, 31 Aug 2026 14:51:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(page):=20=E5=AE=9E=E7=8E=B0=E9=9A=90?= =?UTF-8?q?=E7=A7=81=E4=B8=AD=E5=BF=83=208=20=E5=AD=90=E9=A1=B5=E4=B8=8E?= =?UTF-8?q?=E6=B3=A8=E9=94=80/=E5=88=A0=E6=95=B0=E6=8D=AE/=E9=9A=90?= =?UTF-8?q?=E7=A7=81=E6=94=BF=E7=AD=96/=E7=94=A8=E6=88=B7=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=EF=BC=9B=E7=AB=9E=E4=BA=89=E5=8A=9B=E6=8E=92=E5=90=8D?= =?UTF-8?q?=E4=B8=8E=E5=B2=97=E4=BD=8D=E8=AF=84=E4=BB=B7=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E8=A1=A5=E6=95=B0=E6=8D=AE=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 隐私中心 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 接入,无孤儿页面 --- app/settings/account-cancel.tsx | 350 ++++++++++++++++++++++++++++ app/settings/delete-data.tsx | 373 ++++++++++++++++++++++++++++++ app/settings/privacy-policy.tsx | 5 + app/settings/privacy/[id].tsx | 279 ++++++++++++++++++++++ app/settings/privacy/dnd.tsx | 295 +++++++++++++++++++++++ app/settings/privacy/keywords.tsx | 357 ++++++++++++++++++++++++++++ app/settings/user-agreement.tsx | 5 + src/components/LegalDocScreen.tsx | 204 ++++++++++++++++ 8 files changed, 1868 insertions(+) create mode 100644 app/settings/account-cancel.tsx create mode 100644 app/settings/delete-data.tsx create mode 100644 app/settings/privacy-policy.tsx create mode 100644 app/settings/privacy/[id].tsx create mode 100644 app/settings/privacy/dnd.tsx create mode 100644 app/settings/privacy/keywords.tsx create mode 100644 app/settings/user-agreement.tsx create mode 100644 src/components/LegalDocScreen.tsx diff --git a/app/settings/account-cancel.tsx b/app/settings/account-cancel.tsx new file mode 100644 index 0000000..c7b33be --- /dev/null +++ b/app/settings/account-cancel.tsx @@ -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 ( + + + + + 账号注销 + + + ); +} + +/* ===================== 页面 ===================== */ + +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 ( + +
router.back()} /> + + + + + ); + } + + if (error || !data) { + return ( + +
router.back()} /> + + + 加载失败,请重试 + + 重试 + + + + ); + } + + if (done?.success) { + return ( + +
router.back()} /> + + + {done.message} + router.back()}> + 返回 + + + + ); + } + + return ( + +
router.back()} /> + + + + + 注销后账号将无法恢复,请谨慎操作 + + + + + 注销后将发生 + + {data.consequences.map((item, index) => ( + + + {item} + + ))} + + + + + 注销前待办 + + {data.checklist.map((item, index) => ( + + + {item.label} + {item.actionText ? ( + console.log('[account-cancel] 去处理', item.id)}> + {item.actionText} + + ) : null} + + + + ))} + + + + + 注销原因(选填) + + {data.reasons.map((r) => { + const active = reason === r; + return ( + setReason(active ? '' : r)} + > + {r} + + ); + })} + + + + + + {submitting ? '提交中…' : data.canCancel ? '申请注销' : '请先完成注销前待办'} + + + + + ); +} + +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, + }, +}); diff --git a/app/settings/delete-data.tsx b/app/settings/delete-data.tsx new file mode 100644 index 0000000..4ca382e --- /dev/null +++ b/app/settings/delete-data.tsx @@ -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 ( + + + + + 删除数据 + + + ); +} + +/* ===================== 子组件:勾选行 ===================== */ + +function CheckRow({ + title, + desc, + count, + checked, + onPress, +}: { + title: string; + desc?: string; + count?: string; + checked: boolean; + onPress: () => void; +}) { + return ( + + + {title} + {desc ? {desc} : null} + + + {checked ? : null} + + {count ? {count} : null} + + ); +} + +/* ===================== 页面 ===================== */ + +export default function DeleteAllDataScreen() { + const router = useRouter(); + const { data, loading, error, refresh, submit } = useDeleteAllData(); + const [selected, setSelected] = useState>(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 ( + +
router.back()} /> + + + + + ); + } + + if (error || !data) { + return ( + +
router.back()} /> + + + 加载失败,请重试 + + 重试 + + + + ); + } + + if (done?.success) { + return ( + +
router.back()} /> + + + {done.message} + router.back()}> + 返回 + + + + ); + } + + const canSubmit = selected.size > 0 && confirmText === '删除全部数据' && !submitting; + + return ( + +
router.back()} /> + + + {data.intro} + + + + 选择要删除的数据 + + {data.categories.map((cat) => ( + toggleCat(cat.id)} + /> + ))} + + + + + + {data.warning} + + + + 确认删除 + + 请输入「{data.confirmPlaceholder.replace('请输入「', '').replace('」以确认', '')}」以确认操作 + + + + + + + {submitting ? '提交中…' : `删除选中数据(${selected.size} 项)`} + + + + + ); +} + +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, + }, +}); diff --git a/app/settings/privacy-policy.tsx b/app/settings/privacy-policy.tsx new file mode 100644 index 0000000..7d8f130 --- /dev/null +++ b/app/settings/privacy-policy.tsx @@ -0,0 +1,5 @@ +import { LegalDocScreen } from '../../src/components/LegalDocScreen'; + +export default function PrivacyPolicyScreen() { + return ; +} diff --git a/app/settings/privacy/[id].tsx b/app/settings/privacy/[id].tsx new file mode 100644 index 0000000..ac66568 --- /dev/null +++ b/app/settings/privacy/[id].tsx @@ -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 ( + + + + + + {title} + + + + ); +} + +/* ===================== 子组件:可选项行 ===================== */ + +function OptionRow({ + item, + disabled, + isLast, + onToggle, +}: { + item: PrivacyDetailOption; + disabled: boolean; + isLast: boolean; + onToggle: (id: string, enabled: boolean) => void; +}) { + return ( + + + {item.title} + {item.description ? {item.description} : null} + + onToggle(item.id, next)} + trackColor={{ false: colors.border, true: colors.primaryBorder }} + thumbColor={item.enabled ? colors.primary : colors.white} + /> + + ); +} + +/* ===================== 页面 ===================== */ + +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 ( + +
router.back()} /> + + + + + ); + } + + if (error || !detail) { + return ( + +
router.back()} /> + + + {detail === null ? '暂无该设置项' : '加载失败,请重试'} + + 重试 + + + + ); + } + + return ( + +
router.back()} /> + + + {detail.intro} + + + + 主开关 + + + + {detail.master.title} + + toggle(detail.master.id, next)} + trackColor={{ false: colors.border, true: colors.primaryBorder }} + thumbColor={detail.master.enabled ? colors.primary : colors.white} + /> + + + + + + 细选项 + + {detail.options.map((item, index) => ( + + ))} + + + + {detail.note ? ( + + + {detail.note} + + ) : null} + + + ); +} + +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, + }, +}); diff --git a/app/settings/privacy/dnd.tsx b/app/settings/privacy/dnd.tsx new file mode 100644 index 0000000..077a0b4 --- /dev/null +++ b/app/settings/privacy/dnd.tsx @@ -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 ( + + + + + 勿扰模式 + + + ); +} + +/* ===================== 子组件:时间输入行 ===================== */ + +function TimeRow({ + label, + value, + onChange, +}: { + label: string; + value: string; + onChange: (text: string) => void; +}) { + return ( + + {label} + + + ); +} + +/* ===================== 页面 ===================== */ + +export default function DndSettingScreen() { + const router = useRouter(); + const { setting, loading, error, refresh, update } = useDndSetting(); + + if (loading) { + return ( + +
router.back()} /> + + + + + ); + } + + if (error || !setting) { + return ( + +
router.back()} /> + + + 加载失败,请重试 + + 重试 + + + + ); + } + + return ( + +
router.back()} /> + + + {setting.blockedCount} + 累计拦截的通知条数 + + + + 基础设置 + + + + 开启勿扰模式 + + update({ enabled: next })} + trackColor={{ false: colors.border, true: colors.primaryBorder }} + thumbColor={setting.enabled ? colors.primary : colors.white} + /> + + update({ startTime: text })} + /> + + update({ endTime: text })} + /> + + + + + 高级设置 + + + + 允许重要通知穿透 + 面试邀约等与企业的重要沟通仍会提醒你 + + update({ allowUrgent: next })} + trackColor={{ false: colors.border, true: colors.primaryBorder }} + thumbColor={setting.allowUrgent ? colors.primary : colors.white} + /> + + + + + + + + 开启勿扰模式后,{setting.startTime} 至 {setting.endTime} 期间的非重要通知将被静音。你仍可在「企业沟通」中查看错过的消息。 + + + + + ); +} + +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, + }, +}); diff --git a/app/settings/privacy/keywords.tsx b/app/settings/privacy/keywords.tsx new file mode 100644 index 0000000..f16a038 --- /dev/null +++ b/app/settings/privacy/keywords.tsx @@ -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 ( + + + + + 关键词屏蔽 + + + ); +} + +/* ===================== 子组件:关键词标签 ===================== */ + +function KeywordChip({ + text, + onRemove, +}: { + text: string; + onRemove: () => void; +}) { + return ( + + {text} + + + + + ); +} + +/* ===================== 页面 ===================== */ + +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 ( + +
router.back()} /> + + + + + ); + } + + if (error || !setting) { + return ( + +
router.back()} /> + + + 加载失败,请重试 + + 重试 + + + + ); + } + + return ( + +
router.back()} /> + + + + {setting.blockedCount} + 累计拦截的招聘信息条数 + + + + 已屏蔽关键词 + {setting.keywords.length === 0 ? ( + 暂无屏蔽词,添加后相关岗位与消息将被过滤 + ) : ( + + {setting.keywords.map((word) => ( + removeKeyword(word)} + /> + ))} + + )} + + + {setting.suggestions.length > 0 ? ( + + 系统建议屏蔽 + 点击即可快速添加高频骚扰词 + + {setting.suggestions.map((word) => ( + addKeyword(word)} + > + + {word} + + ))} + + + ) : null} + + + + + 屏蔽关键词后,标题或详情包含该词的职位推荐与沟通消息将被隐藏,不影响你已投递的岗位。 + + + + + + + + 添加 + + + + + ); +} + +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, + }, +}); diff --git a/app/settings/user-agreement.tsx b/app/settings/user-agreement.tsx new file mode 100644 index 0000000..3fd7b66 --- /dev/null +++ b/app/settings/user-agreement.tsx @@ -0,0 +1,5 @@ +import { LegalDocScreen } from '../../src/components/LegalDocScreen'; + +export default function UserAgreementScreen() { + return ; +} diff --git a/src/components/LegalDocScreen.tsx b/src/components/LegalDocScreen.tsx new file mode 100644 index 0000000..32b4f0f --- /dev/null +++ b/src/components/LegalDocScreen.tsx @@ -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 ( + + + + + + {title} + + + + ); +} + +/* ===================== 共享页面:法律文档(隐私政策 / 用户协议) ===================== */ + +export function LegalDocScreen({ + docId, + fallbackTitle, +}: { + docId: string; + fallbackTitle: string; +}) { + const router = useRouter(); + const { doc, loading, error, refresh } = useLegalDocument(docId); + + if (loading) { + return ( + +
router.back()} /> + + + + + ); + } + + if (error || !doc) { + return ( + +
router.back()} /> + + + 加载失败,请重试 + + 重试 + + + + ); + } + + return ( + +
router.back()} /> + + + 版本 {doc.version} + 更新于 {doc.updatedAt} + + + + {doc.preamble} + + + {doc.sections.map((section, index) => ( + + {section.heading} + {section.paragraphs.map((p, pIndex) => ( + + {p} + + ))} + + ))} + + + 本文件含【待确认】占位项,正式发布前需由法务复核并替换公司全称、联系方式、管辖法院等信息。 + + + + ); +} + +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, + }, +});