From 7a239ffe5a298656cd127191246f2dcaa7ce5ef0 Mon Sep 17 00:00:00 2001 From: doublex001 <16157008+doublex001@user.noreply.gitee.com> Date: Thu, 3 Sep 2026 11:50:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A1=A5=E5=85=A8=20P1=20=E5=8D=A0?= =?UTF-8?q?=E4=BD=8D=E9=A1=B5=E4=B8=8E=20P2=20=E7=A9=BA=20onPress=20?= =?UTF-8?q?=E6=8E=A5=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1 收藏/历史占位页补真实数据: - 新增 fetchFavorites/fetchHistory 接口(api.ts + index.ts 登记) - 新增 useFavorites/useHistory hooks 并注册导出 - 新增复用组件 JobCard,favorites/history 接入真实列表+三态+点击进 /jobs/[id] P2 空 onPress 接线: - growth 设置→/settings、文章卡与"更多"→/article/[id] - legal 四分支:chat/templates/faq/upgrade 分别跳对应子页 - 新增 articles 详情页 /article/[id] 与 legal 三子页(faq/upgrade/templates) - appeal 提交成功弹 Alert 并 router.back() --- app/(tabs)/growth.tsx | 6 +- app/appeal.tsx | 5 +- app/article/[id].tsx | 162 ++++++++++++++++++++++++++++++++ app/favorites.tsx | 86 +++++++++++++++-- app/history.tsx | 86 +++++++++++++++-- app/legal.tsx | 6 +- app/legal/faq.tsx | 137 +++++++++++++++++++++++++++ app/legal/templates.tsx | 135 ++++++++++++++++++++++++++ app/legal/upgrade.tsx | 187 +++++++++++++++++++++++++++++++++++++ src/api/api.ts | 36 +++++++ src/api/index.ts | 2 + src/components/JobCard.tsx | 122 ++++++++++++++++++++++++ src/hooks/index.ts | 2 + src/hooks/useFavorites.ts | 17 ++++ src/hooks/useHistory.ts | 17 ++++ 15 files changed, 985 insertions(+), 21 deletions(-) create mode 100644 app/article/[id].tsx create mode 100644 app/legal/faq.tsx create mode 100644 app/legal/templates.tsx create mode 100644 app/legal/upgrade.tsx create mode 100644 src/components/JobCard.tsx create mode 100644 src/hooks/useFavorites.ts create mode 100644 src/hooks/useHistory.ts 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 }; +}