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,
+ },
+});