diff --git a/app/(tabs)/growth.tsx b/app/(tabs)/growth.tsx
index fa6d400..ef984b4 100644
--- a/app/(tabs)/growth.tsx
+++ b/app/(tabs)/growth.tsx
@@ -180,7 +180,7 @@ export default function GrowthScreen() {
style={styles.header}
>
成长中心
- { /* TODO: 设置/更多 */ }}>
+ router.push('/settings')}>
@@ -276,7 +276,7 @@ export default function GrowthScreen() {
{/* 5. 职场干货 */}
职场干货
- { /* TODO: 更多 */ }}>
+ { if (articles[0]) router.push(`/article/${articles[0].id}`); }}>
更多 >
@@ -286,7 +286,7 @@ export default function GrowthScreen() {
{ /* TODO: 阅读文章 */ }}
+ onPress={() => router.push(`/article/${article.id}`)}
>
diff --git a/app/appeal.tsx b/app/appeal.tsx
index 258c57c..53944b6 100644
--- a/app/appeal.tsx
+++ b/app/appeal.tsx
@@ -2,6 +2,7 @@ import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import {
ActivityIndicator,
+ Alert,
Pressable,
ScrollView,
StyleSheet,
@@ -322,7 +323,9 @@ export default function AppealScreen() {
const handleSubmit = async () => {
const result = await submit();
if (result) {
- // TODO: 提交成功后跳转结果页或关闭弹层
+ Alert.alert('提交成功', '我们会在 3 个工作日内完成审核,结果将通过站内消息通知你。', [
+ { text: '确定', onPress: () => router.back() },
+ ]);
}
};
diff --git a/app/article/[id].tsx b/app/article/[id].tsx
new file mode 100644
index 0000000..f058238
--- /dev/null
+++ b/app/article/[id].tsx
@@ -0,0 +1,162 @@
+import { Ionicons } from '@expo/vector-icons';
+import { useLocalSearchParams, useRouter } from 'expo-router';
+import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
+import type { ComponentProps } from 'react';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useGrowthData } from '../../src/hooks';
+import { LoadingState, EmptyState } from '../../src/components/StateView';
+import { colors } from '../../src/theme';
+import { formatViews } from '../../src/utils';
+import type { Article } from '../../src/types';
+
+type IconName = ComponentProps['name'];
+
+const THUMBNAIL_BG: Record = {
+ violet: colors.infoLight,
+ orange: colors.actionLight,
+ green: colors.successLight,
+};
+const THUMBNAIL_COLOR: Record = {
+ violet: colors.info,
+ orange: colors.action,
+ green: colors.success,
+};
+
+/* ===================== 顶部导航 ===================== */
+
+function Header({ onBack }: { onBack: () => void }) {
+ return (
+
+
+
+
+ 职场干货
+
+
+ );
+}
+
+/* ===================== 页面 ===================== */
+
+export default function ArticleScreen() {
+ const router = useRouter();
+ const { id } = useLocalSearchParams<{ id: string }>();
+ const { articles, loading, error, refresh } = useGrowthData();
+
+ const article = articles.find((a) => a.id === id);
+
+ return (
+
+ router.back()} />
+ {loading ? (
+
+ ) : error ? (
+
+ ) : !article ? (
+
+ ) : (
+
+
+
+
+ {article.title}
+
+ {article.tag}
+ {formatViews(article.views)}阅读
+
+
+ {`在求职与职业发展的道路上,清晰的自我认知与扎实的准备同样重要。本文围绕「${article.title}」展开,帮助你建立系统的应对思路——从自我定位、能力补齐到面试表达,形成可落地的行动清单。\n\n(以下为正文占位内容,后端就绪后由接口返回真实文章正文。)`}
+
+
+ )}
+
+ );
+}
+
+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: {
+ fontSize: 17,
+ fontWeight: '700',
+ color: colors.textPrimary,
+ },
+ scroll: {
+ flex: 1,
+ },
+ content: {
+ padding: 16,
+ paddingBottom: 24,
+ },
+ thumb: {
+ width: 72,
+ height: 72,
+ borderRadius: 16,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginBottom: 14,
+ },
+ title: {
+ fontSize: 20,
+ fontWeight: '700',
+ color: colors.textPrimary,
+ lineHeight: 28,
+ },
+ meta: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginTop: 10,
+ marginBottom: 16,
+ },
+ tag: {
+ fontSize: 11,
+ color: colors.primaryText,
+ backgroundColor: colors.primaryLight,
+ paddingHorizontal: 8,
+ paddingVertical: 2,
+ borderRadius: 8,
+ overflow: 'hidden',
+ marginRight: 10,
+ },
+ views: {
+ fontSize: 12,
+ color: colors.textHint,
+ },
+ body: {
+ fontSize: 14,
+ lineHeight: 24,
+ color: colors.textSecondary,
+ },
+});
diff --git a/app/favorites.tsx b/app/favorites.tsx
index e123e71..429b911 100644
--- a/app/favorites.tsx
+++ b/app/favorites.tsx
@@ -1,14 +1,86 @@
-import { EmptyState } from '../src/components/StateView';
+import { Ionicons } from '@expo/vector-icons';
+import { useRouter } from 'expo-router';
+import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useFavorites } from '../src/hooks';
+import { JobCard } from '../src/components/JobCard';
+import { LoadingState, ErrorState, EmptyState } from '../src/components/StateView';
+import { colors } from '../src/theme';
/**
- * 我的收藏页(占位)
+ * 我的收藏页(职位)
*/
export default function FavoritesScreen() {
+ const router = useRouter();
+ const { items, loading, error, refresh, isEmpty } = useFavorites();
+
return (
-
+
+
+ router.back()} hitSlop={8}>
+
+
+ 我的收藏
+
+
+
+ {loading ? (
+
+ ) : error ? (
+
+ ) : isEmpty ? (
+
+ ) : (
+
+ {items.map((job) => (
+ router.push(`/jobs/${job.id}`)} />
+ ))}
+
+ )}
+
);
}
+
+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: {
+ fontSize: 17,
+ fontWeight: '700',
+ color: colors.textPrimary,
+ },
+ scroll: {
+ flex: 1,
+ },
+ content: {
+ paddingHorizontal: 12,
+ paddingTop: 12,
+ paddingBottom: 24,
+ },
+});
diff --git a/app/history.tsx b/app/history.tsx
index a43e9ab..c243bf4 100644
--- a/app/history.tsx
+++ b/app/history.tsx
@@ -1,14 +1,86 @@
-import { EmptyState } from '../src/components/StateView';
+import { Ionicons } from '@expo/vector-icons';
+import { useRouter } from 'expo-router';
+import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useHistory } from '../src/hooks';
+import { JobCard } from '../src/components/JobCard';
+import { LoadingState, ErrorState, EmptyState } from '../src/components/StateView';
+import { colors } from '../src/theme';
/**
- * 浏览历史页(占位)
+ * 浏览历史页(职位)
*/
export default function HistoryScreen() {
+ const router = useRouter();
+ const { items, loading, error, refresh, isEmpty } = useHistory();
+
return (
-
+
+
+ router.back()} hitSlop={8}>
+
+
+ 浏览历史
+
+
+
+ {loading ? (
+
+ ) : error ? (
+
+ ) : isEmpty ? (
+
+ ) : (
+
+ {items.map((job) => (
+ router.push(`/jobs/${job.id}`)} />
+ ))}
+
+ )}
+
);
}
+
+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: {
+ fontSize: 17,
+ fontWeight: '700',
+ color: colors.textPrimary,
+ },
+ scroll: {
+ flex: 1,
+ },
+ content: {
+ paddingHorizontal: 12,
+ paddingTop: 12,
+ paddingBottom: 24,
+ },
+});
diff --git a/app/legal.tsx b/app/legal.tsx
index 80e8072..8748550 100644
--- a/app/legal.tsx
+++ b/app/legal.tsx
@@ -146,11 +146,11 @@ export default function LegalScreen() {
if (service.action === 'chat') {
router.push('/legal/chat');
} else if (service.action === 'templates') {
- // 模板在首页已展示,滚动到模板区更自然;这里简单提示
+ router.push('/legal/templates');
} else if (service.action === 'faq') {
- // TODO: 接入 FAQ 列表页
+ router.push('/legal/faq');
} else if (service.action === 'upgrade') {
- // TODO: 接入付费升级入口
+ router.push('/legal/upgrade');
}
};
diff --git a/app/legal/faq.tsx b/app/legal/faq.tsx
new file mode 100644
index 0000000..b50c07e
--- /dev/null
+++ b/app/legal/faq.tsx
@@ -0,0 +1,137 @@
+import { Ionicons } from '@expo/vector-icons';
+import { useRouter } from 'expo-router';
+import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useState } from 'react';
+import { useLegalData } from '../../src/hooks';
+import { LoadingState, ErrorState, EmptyState } from '../../src/components/StateView';
+import { colors } from '../../src/theme';
+import type { LegalFAQ } from '../../src/types';
+
+/* ===================== 顶部导航 ===================== */
+
+function Header({ onBack }: { onBack: () => void }) {
+ return (
+
+
+
+
+ 常见问题
+
+
+ );
+}
+
+/* ===================== FAQ 项 ===================== */
+
+function FAQItem({ faq }: { faq: LegalFAQ }) {
+ const [expanded, setExpanded] = useState(false);
+ return (
+ setExpanded((v) => !v)}>
+
+ {faq.question}
+
+
+ {expanded && {faq.answer}}
+
+ );
+}
+
+/* ===================== 页面 ===================== */
+
+export default function LegalFaqScreen() {
+ const router = useRouter();
+ const { faqs, loading, error, refresh } = useLegalData();
+
+ return (
+
+ router.back()} />
+ {loading ? (
+
+ ) : error ? (
+
+ ) : faqs.length === 0 ? (
+
+ ) : (
+
+
+ {faqs.map((faq) => (
+
+ ))}
+
+
+ )}
+
+ );
+}
+
+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: {
+ fontSize: 17,
+ fontWeight: '700',
+ color: colors.textPrimary,
+ },
+ scroll: {
+ flex: 1,
+ },
+ content: {
+ padding: 12,
+ paddingBottom: 24,
+ },
+ faqCard: {
+ backgroundColor: colors.white,
+ borderRadius: 8,
+ paddingHorizontal: 12,
+ },
+ faqItem: {
+ paddingVertical: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.borderLight,
+ },
+ faqRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ faqQ: {
+ flex: 1,
+ fontSize: 14,
+ fontWeight: '600',
+ color: colors.textPrimary,
+ marginRight: 8,
+ },
+ faqA: {
+ marginTop: 8,
+ fontSize: 13,
+ lineHeight: 19,
+ color: colors.textSecondary,
+ },
+});
diff --git a/app/legal/templates.tsx b/app/legal/templates.tsx
new file mode 100644
index 0000000..2d50346
--- /dev/null
+++ b/app/legal/templates.tsx
@@ -0,0 +1,135 @@
+import { Ionicons } from '@expo/vector-icons';
+import { useRouter } from 'expo-router';
+import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useLegalData } from '../../src/hooks';
+import { LoadingState, ErrorState, EmptyState } from '../../src/components/StateView';
+import { colors } from '../../src/theme';
+import type { LegalTemplate } from '../../src/types';
+
+/* ===================== 顶部导航 ===================== */
+
+function Header({ onBack }: { onBack: () => void }) {
+ return (
+
+
+
+
+ 文书模板
+
+
+ );
+}
+
+/* ===================== 模板卡片 ===================== */
+
+function TemplateCard({ template, onPress }: { template: LegalTemplate; onPress: () => void }) {
+ return (
+
+
+
+ {template.name}
+ {template.description}
+
+
+
+ );
+}
+
+/* ===================== 页面 ===================== */
+
+export default function LegalTemplatesScreen() {
+ const router = useRouter();
+ const { templates, loading, error, refresh } = useLegalData();
+
+ return (
+
+ router.back()} />
+ {loading ? (
+
+ ) : error ? (
+
+ ) : templates.length === 0 ? (
+
+ ) : (
+
+
+ {templates.map((template) => (
+
+ Alert.alert('文书预览', `「${template.name}」预览与下载功能即将上线`)
+ }
+ />
+ ))}
+
+
+ )}
+
+ );
+}
+
+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: {
+ fontSize: 17,
+ fontWeight: '700',
+ color: colors.textPrimary,
+ },
+ scroll: {
+ flex: 1,
+ },
+ content: {
+ padding: 12,
+ paddingBottom: 24,
+ },
+ templateGrid: {
+ gap: 8,
+ },
+ templateCard: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 12,
+ borderRadius: 8,
+ backgroundColor: colors.white,
+ },
+ templateBody: {
+ flex: 1,
+ marginLeft: 12,
+ marginRight: 8,
+ },
+ templateName: {
+ fontSize: 14,
+ fontWeight: '700',
+ color: colors.textPrimary,
+ },
+ templateDesc: {
+ marginTop: 2,
+ fontSize: 12,
+ color: colors.textSecondary,
+ },
+});
diff --git a/app/legal/upgrade.tsx b/app/legal/upgrade.tsx
new file mode 100644
index 0000000..7c94cd5
--- /dev/null
+++ b/app/legal/upgrade.tsx
@@ -0,0 +1,187 @@
+import { Ionicons } from '@expo/vector-icons';
+import { useRouter } from 'expo-router';
+import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { useLegalData } from '../../src/hooks';
+import { LoadingState, ErrorState, EmptyState } from '../../src/components/StateView';
+import { colors } from '../../src/theme';
+import type { LegalPlan } from '../../src/types';
+
+/* ===================== 顶部导航 ===================== */
+
+function Header({ onBack }: { onBack: () => void }) {
+ return (
+
+
+
+
+ 开通会员
+
+
+ );
+}
+
+/* ===================== 套餐卡片 ===================== */
+
+function PlanCard({ plan }: { plan: LegalPlan }) {
+ return (
+
+ {plan.recommended && (
+
+ 推荐
+
+ )}
+ {plan.name}
+
+ ¥
+ {plan.price}
+ {plan.period}
+
+
+ {plan.benefits.map((benefit, idx) => (
+
+
+ {benefit}
+
+ ))}
+
+
+ );
+}
+
+/* ===================== 页面 ===================== */
+
+export default function LegalUpgradeScreen() {
+ const router = useRouter();
+ const { plans, loading, error, refresh } = useLegalData();
+
+ return (
+
+ router.back()} />
+ {loading ? (
+
+ ) : error ? (
+
+ ) : plans.length === 0 ? (
+
+ ) : (
+
+ 开通会员后可享受律师咨询、合同审核等专属法律服务
+
+ {plans.map((plan) => (
+
+ ))}
+
+
+ )}
+
+ );
+}
+
+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: {
+ fontSize: 17,
+ fontWeight: '700',
+ color: colors.textPrimary,
+ },
+ scroll: {
+ flex: 1,
+ },
+ content: {
+ padding: 12,
+ paddingBottom: 24,
+ },
+ tip: {
+ fontSize: 13,
+ color: colors.textSecondary,
+ marginBottom: 12,
+ },
+ planGrid: {
+ gap: 12,
+ },
+ planCard: {
+ padding: 14,
+ borderRadius: 8,
+ backgroundColor: colors.white,
+ borderWidth: 1,
+ borderColor: colors.border,
+ },
+ planCardRecommended: {
+ borderColor: colors.action,
+ backgroundColor: colors.actionLight,
+ },
+ recommendedTag: {
+ alignSelf: 'flex-start',
+ paddingHorizontal: 6,
+ paddingVertical: 2,
+ borderRadius: 4,
+ backgroundColor: colors.action,
+ marginBottom: 8,
+ },
+ recommendedText: {
+ fontSize: 10,
+ fontWeight: '700',
+ color: colors.white,
+ },
+ planName: {
+ fontSize: 15,
+ fontWeight: '700',
+ color: colors.textPrimary,
+ },
+ planPriceRow: {
+ flexDirection: 'row',
+ alignItems: 'baseline',
+ marginTop: 8,
+ marginBottom: 10,
+ },
+ planCurrency: {
+ fontSize: 14,
+ fontWeight: '700',
+ color: colors.actionText,
+ },
+ planPrice: {
+ fontSize: 28,
+ fontWeight: '700',
+ color: colors.actionText,
+ },
+ planPeriod: {
+ fontSize: 13,
+ color: colors.textSecondary,
+ },
+ benefits: {
+ gap: 6,
+ },
+ benefitRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
+ },
+ benefitText: {
+ fontSize: 12,
+ color: colors.textSecondary,
+ },
+});
diff --git a/src/api/api.ts b/src/api/api.ts
index 6fc310a..1b85a64 100644
--- a/src/api/api.ts
+++ b/src/api/api.ts
@@ -912,6 +912,42 @@ export async function fetchJobList(
});
}
+/* ===================== 我的收藏 / 浏览历史(mock 数据) ===================== */
+
+/** 收藏的职位 id 列表(mock 阶段取 MOCK_JOBS 子集) */
+const MOCK_FAVORITE_IDS = ['1', '3', '6'];
+/** 浏览历史的职位 id 列表(mock 阶段取 MOCK_JOBS 子集) */
+const MOCK_HISTORY_IDS = ['2', '4', '5', '7'];
+
+const MOCK_FAVORITES: Omit[] = MOCK_JOBS.filter((job) =>
+ MOCK_FAVORITE_IDS.includes(job.id),
+);
+const MOCK_HISTORY: Omit[] = MOCK_JOBS.filter((job) =>
+ MOCK_HISTORY_IDS.includes(job.id),
+);
+
+/**
+ * 获取我的收藏(职位)列表
+ * @method GET
+ * @url /api/user/favorites
+ * @returns Promise[]> 收藏的职位数组
+ */
+export async function fetchFavorites(): Promise[]> {
+ // TODO: const res = await fetch(`${API_BASE_URL}/api/user/favorites`, { method: 'GET' }); return res.json();
+ return mockRequest(MOCK_FAVORITES);
+}
+
+/**
+ * 获取浏览历史(职位)列表
+ * @method GET
+ * @url /api/user/history
+ * @returns Promise[]> 浏览过的职位数组(按时间倒序)
+ */
+export async function fetchHistory(): Promise[]> {
+ // TODO: const res = await fetch(`${API_BASE_URL}/api/user/history`, { method: 'GET' }); return res.json();
+ return mockRequest(MOCK_HISTORY);
+}
+
/**
* 获取会话列表
* @method GET
diff --git a/src/api/index.ts b/src/api/index.ts
index 3efaa45..c48c88d 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -45,6 +45,8 @@ export {
fetchInterviews,
fetchJobDetail,
fetchJobList,
+ fetchFavorites,
+ fetchHistory,
fetchOfferCompareData,
fetchJobOverallScore,
fetchJobPreference,
diff --git a/src/components/JobCard.tsx b/src/components/JobCard.tsx
new file mode 100644
index 0000000..121118e
--- /dev/null
+++ b/src/components/JobCard.tsx
@@ -0,0 +1,122 @@
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+import { colors } from '../theme';
+import type { Job } from '../types';
+
+/** 收藏 / 历史等列表复用的职位摘要卡片(不含 level 字段) */
+export type JobSummary = Omit;
+
+/* ===================== 职位卡片 =====================
+ * 仅展示列表态所需的字段(公司、职位、薪资、城市、评分、福利标签),
+ * 点击进入岗位详情 /jobs/[id]。颜色全部走主题 token,遵守 AGENTS.md 配色约束。
+ */
+export function JobCard({ job, onPress }: { job: JobSummary; onPress: () => void }) {
+ return (
+
+
+
+ {job.companyName}
+
+
+ {job.score}
+
+
+
+
+ {job.positionName}
+
+
+
+
+ {job.salaryMin}-{job.salaryMax}K
+
+ ·
+ {job.city}
+ ·
+ {job.experience}
+
+
+
+ {job.benefits.slice(0, 3).map((benefit) => (
+
+ {benefit}
+
+ ))}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ card: {
+ backgroundColor: colors.white,
+ borderRadius: 12,
+ padding: 14,
+ marginBottom: 10,
+ borderWidth: 1,
+ borderColor: colors.borderLight,
+ },
+ top: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ company: {
+ flex: 1,
+ fontSize: 14,
+ fontWeight: '700',
+ color: colors.textPrimary,
+ marginRight: 8,
+ },
+ scoreBadge: {
+ paddingHorizontal: 8,
+ paddingVertical: 2,
+ borderRadius: 8,
+ backgroundColor: colors.primaryLight,
+ },
+ scoreText: {
+ fontSize: 13,
+ fontWeight: '700',
+ color: colors.primaryText,
+ },
+ position: {
+ marginTop: 6,
+ fontSize: 16,
+ fontWeight: '600',
+ color: colors.textPrimary,
+ },
+ meta: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginTop: 8,
+ },
+ salary: {
+ fontSize: 14,
+ fontWeight: '700',
+ color: colors.primaryText,
+ },
+ sep: {
+ marginHorizontal: 6,
+ fontSize: 12,
+ color: colors.textHint,
+ },
+ metaText: {
+ fontSize: 12,
+ color: colors.textSecondary,
+ },
+ tags: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 6,
+ marginTop: 10,
+ },
+ tag: {
+ paddingHorizontal: 8,
+ paddingVertical: 3,
+ borderRadius: 6,
+ backgroundColor: colors.borderLight,
+ },
+ tagText: {
+ fontSize: 11,
+ color: colors.textSecondary,
+ },
+});
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index 43f123e..687d876 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -28,6 +28,8 @@ export { useJobDetail } from './useJobDetail';
export { useJobEvaluationEmptyState } from './useJobEvaluationEmptyState';
export { useJobEvaluations } from './useJobEvaluations';
export { useJobList } from './useJobList';
+export { useFavorites } from './useFavorites';
+export { useHistory } from './useHistory';
export { useJobManagement } from './useJobManagement';
export { useJobPreference } from './useJobPreference';
export { useJobProgress } from './useJobProgress';
diff --git a/src/hooks/useFavorites.ts b/src/hooks/useFavorites.ts
new file mode 100644
index 0000000..161b71f
--- /dev/null
+++ b/src/hooks/useFavorites.ts
@@ -0,0 +1,17 @@
+/**
+ * 我的收藏(职位)数据 hook:复用通用异步取数 hook,统一管理三态。
+ */
+import type { Job } from '../types';
+import { fetchFavorites } from '../api';
+import { useAsyncData } from './useAsyncData';
+
+export type FavoriteJob = Omit;
+
+export function useFavorites() {
+ const { data, loading, error, refresh } = useAsyncData(fetchFavorites);
+ const items = data ?? [];
+ // 空状态:加载完成且无错误、但列表为空
+ const isEmpty = !loading && !error && items.length === 0;
+
+ return { items, loading, error, refresh, isEmpty };
+}
diff --git a/src/hooks/useHistory.ts b/src/hooks/useHistory.ts
new file mode 100644
index 0000000..2a7b818
--- /dev/null
+++ b/src/hooks/useHistory.ts
@@ -0,0 +1,17 @@
+/**
+ * 浏览历史(职位)数据 hook:复用通用异步取数 hook,统一管理三态。
+ */
+import type { Job } from '../types';
+import { fetchHistory } from '../api';
+import { useAsyncData } from './useAsyncData';
+
+export type HistoryJob = Omit;
+
+export function useHistory() {
+ const { data, loading, error, refresh } = useAsyncData(fetchHistory);
+ const items = data ?? [];
+ // 空状态:加载完成且无错误、但列表为空
+ const isEmpty = !loading && !error && items.length === 0;
+
+ return { items, loading, error, refresh, isEmpty };
+}