From 87ad5e629c6d4b128208ebcd593b8766313993bb Mon Sep 17 00:00:00 2001 From: doublex001 <16157008+doublex001@user.noreply.gitee.com> Date: Mon, 31 Aug 2026 16:59:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8C=BA=E5=88=86=E4=BC=81=E4=B8=9A?= =?UTF-8?q?=E7=AB=AF=E4=B8=8E=E6=B1=82=E8=81=8C=E7=AB=AF=E5=AF=BC=E8=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 RoleContext(会话级角色持久化),identity-select 进入时写入 role - 新增 app/(enterprise) 路由组 + 企业端底部 Tab 栏(概览/岗位/人才/沟通,success 绿) - 将 10 个企业端页面迁移至 (enterprise) 组(路由路径不变),修正相对引用 - (tabs) 求职端挂载时 setRole('candidate'),两端体感对称 --- app/(enterprise)/_layout.tsx | 208 ++++++++++++++++++ app/{ => (enterprise)}/audit-report.tsx | 6 +- app/{ => (enterprise)}/candidate-detail.tsx | 6 +- .../enterprise-certification.tsx | 6 +- app/{ => (enterprise)}/enterprise-chat.tsx | 6 +- .../enterprise-overview.tsx | 6 +- app/{ => (enterprise)}/hr-evaluation.tsx | 6 +- app/{ => (enterprise)}/job-management.tsx | 16 +- app/{ => (enterprise)}/job-publish.tsx | 6 +- app/{ => (enterprise)}/operation-rules.tsx | 6 +- .../talent-recommendation.tsx | 10 +- app/(tabs)/_layout.tsx | 7 + app/_layout.tsx | 6 +- app/identity-select.tsx | 6 +- src/context/RoleContext.tsx | 77 +++++++ 15 files changed, 338 insertions(+), 40 deletions(-) create mode 100644 app/(enterprise)/_layout.tsx rename app/{ => (enterprise)}/audit-report.tsx (99%) rename app/{ => (enterprise)}/candidate-detail.tsx (99%) rename app/{ => (enterprise)}/enterprise-certification.tsx (98%) rename app/{ => (enterprise)}/enterprise-chat.tsx (99%) rename app/{ => (enterprise)}/enterprise-overview.tsx (98%) rename app/{ => (enterprise)}/hr-evaluation.tsx (99%) rename app/{ => (enterprise)}/job-management.tsx (96%) rename app/{ => (enterprise)}/job-publish.tsx (99%) rename app/{ => (enterprise)}/operation-rules.tsx (98%) rename app/{ => (enterprise)}/talent-recommendation.tsx (98%) create mode 100644 src/context/RoleContext.tsx diff --git a/app/(enterprise)/_layout.tsx b/app/(enterprise)/_layout.tsx new file mode 100644 index 0000000..c159353 --- /dev/null +++ b/app/(enterprise)/_layout.tsx @@ -0,0 +1,208 @@ +import { Ionicons } from '@expo/vector-icons'; +import { Tabs } from 'expo-router/js-tabs'; +import type { BottomTabBarProps } from 'expo-router/js-tabs'; +import { useEffect, useRef } from 'react'; +import { Animated, Easing, Pressable, StyleSheet, View } from 'react-native'; +import { colors } from '../../src/theme'; +import { useRole } from '../../src/context/RoleContext'; + +/** 企业端默认落地「概览」Tab */ +export const unstable_settings = { + initialRouteName: 'enterprise-overview', +}; + +/** 企业端用 success(绿) 区分求职端 primary(蓝);未选中 -outline,选中填充实心 */ +const ICON_MAP = { + 'enterprise-overview': { outline: 'grid-outline', filled: 'grid' }, + 'job-management': { outline: 'folder-outline', filled: 'folder' }, + 'talent-recommendation': { outline: 'people-outline', filled: 'people' }, + 'enterprise-chat': { outline: 'chatbubble-outline', filled: 'chatbubble' }, +} as const; + +type TabRoute = keyof typeof ICON_MAP; + +function TabItem({ + focused, + route, + label, + onPress, + onLongPress, +}: { + focused: boolean; + route: TabRoute; + label: string; + onPress: () => void; + onLongPress: () => void; +}) { + const { outline, filled } = ICON_MAP[route]; + const translateY = useRef(new Animated.Value(focused ? -2 : 0)).current; + const progress = useRef(new Animated.Value(focused ? 1 : 0)).current; + + useEffect(() => { + const translateAnim = Animated.timing(translateY, { + toValue: focused ? -2 : 0, + duration: 150, + easing: Easing.out(Easing.ease), + useNativeDriver: true, + }); + const progressAnim = Animated.timing(progress, { + toValue: focused ? 1 : 0, + duration: 200, + easing: Easing.inOut(Easing.ease), + useNativeDriver: true, + }); + translateAnim.start(); + progressAnim.start(); + return () => { + translateAnim.stop(); + progressAnim.stop(); + }; + }, [focused, translateY, progress]); + + const inactiveOpacity = progress.interpolate({ + inputRange: [0, 1], + outputRange: [1, 0], + }); + + return ( + [styles.tabItem, pressed && styles.tabItemPressed]} + onPress={onPress} + onLongPress={onLongPress} + accessibilityRole="button" + accessibilityState={{ selected: focused }} + > + + + + + + + + + + + + + + {label} + + + {label} + + + + ); +} + +function TabBar({ state, descriptors, navigation, insets }: BottomTabBarProps) { + return ( + + {state.routes.map((route, index) => { + const { options } = descriptors[route.key]; + const focused = state.index === index; + const label = + typeof options.tabBarLabel === 'string' ? options.tabBarLabel : options.title ?? ''; + + const onPress = () => { + const event = navigation.emit({ + type: 'tabPress', + target: route.key, + canPreventDefault: true, + }); + if (!focused && !event.defaultPrevented) { + navigation.navigate(route.name); + } + }; + const onLongPress = () => navigation.emit({ type: 'tabLongPress', target: route.key }); + + return ( + + ); + })} + + ); +} + +/** + * 企业端底部 Tab 导航:概览 / 岗位 / 人才 / 沟通 + * 绿色 success 强调,与求职端蓝色 primary 形成视觉区分。 + */ +export default function EnterpriseTabLayout() { + const { setRole } = useRole(); + + useEffect(() => { + setRole('enterprise'); + }, [setRole]); + + return ( + } + > + + + + + + ); +} + +const styles = StyleSheet.create({ + tabBar: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.white, + paddingTop: 4, + }, + tabItem: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 2, + }, + tabItemPressed: { + transform: [{ scale: 0.92 }], + }, + iconBox: { + width: 22, + height: 22, + }, + center: { + alignItems: 'center', + justifyContent: 'center', + }, + labelBox: { + height: 14, + marginTop: 1, + alignSelf: 'stretch', + }, + labelLayer: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + }, + tabLabel: { + fontSize: 11, + lineHeight: 14, + fontWeight: '400', + textAlign: 'center', + color: colors.textHint, + }, + tabLabelActive: { + fontWeight: '600', + color: colors.success, + }, +}); diff --git a/app/audit-report.tsx b/app/(enterprise)/audit-report.tsx similarity index 99% rename from app/audit-report.tsx rename to app/(enterprise)/audit-report.tsx index 0edf664..d95434c 100644 --- a/app/audit-report.tsx +++ b/app/(enterprise)/audit-report.tsx @@ -3,9 +3,9 @@ import { useRouter } from 'expo-router'; import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import Svg, { Line, Path, Rect, Text as SvgText } from 'react-native-svg'; -import { useAuditReport } from '../src/hooks'; -import { colors } from '../src/theme'; -import type { AuditKPI, AuditTrendPoint, RiskDistributionItem } from '../src/types'; +import { useAuditReport } from '../../src/hooks'; +import { colors } from '../../src/theme'; +import type { AuditKPI, AuditTrendPoint, RiskDistributionItem } from '../../src/types'; /* ===================== 子组件:顶部导航 ===================== */ diff --git a/app/candidate-detail.tsx b/app/(enterprise)/candidate-detail.tsx similarity index 99% rename from app/candidate-detail.tsx rename to app/(enterprise)/candidate-detail.tsx index 2d7e302..a3c7887 100644 --- a/app/candidate-detail.tsx +++ b/app/(enterprise)/candidate-detail.tsx @@ -10,9 +10,9 @@ import { } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import Svg, { Line, Path, Polygon, Text as SvgText } from 'react-native-svg'; -import { useCandidateDetail } from '../src/hooks'; -import { colors } from '../src/theme'; -import type { CandidateAbility, CandidateReview } from '../src/types'; +import { useCandidateDetail } from '../../src/hooks'; +import { colors } from '../../src/theme'; +import type { CandidateAbility, CandidateReview } from '../../src/types'; /* ===================== 子组件:顶部导航 ===================== */ diff --git a/app/enterprise-certification.tsx b/app/(enterprise)/enterprise-certification.tsx similarity index 98% rename from app/enterprise-certification.tsx rename to app/(enterprise)/enterprise-certification.tsx index 68885e8..6e96c69 100644 --- a/app/enterprise-certification.tsx +++ b/app/(enterprise)/enterprise-certification.tsx @@ -11,9 +11,9 @@ import { View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useEnterpriseCertification } from '../src/hooks'; -import { colors } from '../src/theme'; -import type { EnterpriseCertificationForm, EnterpriseCertificationStatus } from '../src/types'; +import { useEnterpriseCertification } from '../../src/hooks'; +import { colors } from '../../src/theme'; +import type { EnterpriseCertificationForm, EnterpriseCertificationStatus } from '../../src/types'; /* ===================== 子组件:顶部导航 ===================== */ diff --git a/app/enterprise-chat.tsx b/app/(enterprise)/enterprise-chat.tsx similarity index 99% rename from app/enterprise-chat.tsx rename to app/(enterprise)/enterprise-chat.tsx index e60eb1c..4b43016 100644 --- a/app/enterprise-chat.tsx +++ b/app/(enterprise)/enterprise-chat.tsx @@ -10,9 +10,9 @@ import { View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useEnterpriseCommunication } from '../src/hooks'; -import { colors } from '../src/theme'; -import type { EnterpriseChatItem, EnterpriseCommunicationTab, PendingTask } from '../src/types'; +import { useEnterpriseCommunication } from '../../src/hooks'; +import { colors } from '../../src/theme'; +import type { EnterpriseChatItem, EnterpriseCommunicationTab, PendingTask } from '../../src/types'; /* ===================== 子组件:顶部导航 ===================== */ diff --git a/app/enterprise-overview.tsx b/app/(enterprise)/enterprise-overview.tsx similarity index 98% rename from app/enterprise-overview.tsx rename to app/(enterprise)/enterprise-overview.tsx index 30d22a0..05a2c65 100644 --- a/app/enterprise-overview.tsx +++ b/app/(enterprise)/enterprise-overview.tsx @@ -10,9 +10,9 @@ import { View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useEnterpriseOverview } from '../src/hooks'; -import { colors, headerGradient } from '../src/theme'; -import type { EnterpriseOverview, EnterpriseQuickAction, EnterpriseStat, JobEvaluationSummary } from '../src/types'; +import { useEnterpriseOverview } from '../../src/hooks'; +import { colors, headerGradient } from '../../src/theme'; +import type { EnterpriseOverview, EnterpriseQuickAction, EnterpriseStat, JobEvaluationSummary } from '../../src/types'; const COLOR_TOKENS: Record = { primary: colors.primary, diff --git a/app/hr-evaluation.tsx b/app/(enterprise)/hr-evaluation.tsx similarity index 99% rename from app/hr-evaluation.tsx rename to app/(enterprise)/hr-evaluation.tsx index ba15b4e..12f810f 100644 --- a/app/hr-evaluation.tsx +++ b/app/(enterprise)/hr-evaluation.tsx @@ -10,9 +10,9 @@ import { } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import Svg, { Circle, Line, Path, Text as SvgText } from 'react-native-svg'; -import { useHREvaluation } from '../src/hooks'; -import { colors } from '../src/theme'; -import type { EvaluationDimension, EvaluationReview, EvaluationTrendPoint } from '../src/types'; +import { useHREvaluation } from '../../src/hooks'; +import { colors } from '../../src/theme'; +import type { EvaluationDimension, EvaluationReview, EvaluationTrendPoint } from '../../src/types'; /** 评级 → 文字颜色(优秀=品牌蓝,良好=主色,其他=次要文字) */ const LEVEL_TEXT_COLOR: Record = { diff --git a/app/job-management.tsx b/app/(enterprise)/job-management.tsx similarity index 96% rename from app/job-management.tsx rename to app/(enterprise)/job-management.tsx index 86d93d6..4327d81 100644 --- a/app/job-management.tsx +++ b/app/(enterprise)/job-management.tsx @@ -11,9 +11,9 @@ import { View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useJobManagement } from '../src/hooks'; -import { colors } from '../src/theme'; -import type { JobManagement, JobManagementJob, JobManagementTab } from '../src/types'; +import { useJobManagement } from '../../src/hooks'; +import { colors } from '../../src/theme'; +import type { JobManagement, JobManagementJob, JobManagementTab } from '../../src/types'; /* ===================== 子组件:顶部导航 ===================== */ @@ -117,11 +117,11 @@ function SearchFilterBar() { placeholderTextColor={colors.textHint} /> - console.log('筛选')}> + {}}> 筛选 - console.log('最新发布')}> + {}}> 最新发布 @@ -178,15 +178,15 @@ function JobCard({ job }: { job: JobManagementJob }) { - console.log('编辑', job.id)}> + {}}> 编辑 - console.log('刷新', job.id)}> + {}}> 刷新 - console.log('下架', job.id)}> + {}}> 下架 diff --git a/app/job-publish.tsx b/app/(enterprise)/job-publish.tsx similarity index 99% rename from app/job-publish.tsx rename to app/(enterprise)/job-publish.tsx index c47bbe6..496f419 100644 --- a/app/job-publish.tsx +++ b/app/(enterprise)/job-publish.tsx @@ -11,9 +11,9 @@ import { View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useJobPublish } from '../src/hooks'; -import { colors } from '../src/theme'; -import type { JobPublishFormData } from '../src/types'; +import { useJobPublish } from '../../src/hooks'; +import { colors } from '../../src/theme'; +import type { JobPublishFormData } from '../../src/types'; const WORK_TYPE_OPTIONS: { label: string; value: JobPublishFormData['workType'] }[] = [ { label: '全职', value: 'full-time' }, diff --git a/app/operation-rules.tsx b/app/(enterprise)/operation-rules.tsx similarity index 98% rename from app/operation-rules.tsx rename to app/(enterprise)/operation-rules.tsx index b3435d7..1336cca 100644 --- a/app/operation-rules.tsx +++ b/app/(enterprise)/operation-rules.tsx @@ -12,9 +12,9 @@ import { View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useOperationRules } from '../src/hooks'; -import { colors } from '../src/theme'; -import type { MatchMode, OperationRules } from '../src/types'; +import { useOperationRules } from '../../src/hooks'; +import { colors } from '../../src/theme'; +import type { MatchMode, OperationRules } from '../../src/types'; /* ===================== 子组件:顶部导航 ===================== */ diff --git a/app/talent-recommendation.tsx b/app/(enterprise)/talent-recommendation.tsx similarity index 98% rename from app/talent-recommendation.tsx rename to app/(enterprise)/talent-recommendation.tsx index 5b460c3..e8e7416 100644 --- a/app/talent-recommendation.tsx +++ b/app/(enterprise)/talent-recommendation.tsx @@ -10,9 +10,9 @@ import { View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useTalentRecommendation } from '../src/hooks'; -import { colors } from '../src/theme'; -import type { TalentCandidate, TalentRecommendationTab } from '../src/types'; +import { useTalentRecommendation } from '../../src/hooks'; +import { colors } from '../../src/theme'; +import type { TalentCandidate, TalentRecommendationTab } from '../../src/types'; /* ===================== 子组件:顶部导航 ===================== */ @@ -264,7 +264,7 @@ export default function TalentRecommendationScreen() { console.log('[TalentRecommendation] 查看人才市场报告')} + onReport={() => {}} /> {recommendation.candidates.map((candidate) => ( @@ -275,7 +275,7 @@ export default function TalentRecommendationScreen() { console.log('[TalentRecommendation] 调整筛选条件')} + onFilter={() => {}} /> )} diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 4f39c5f..2bbb289 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -4,6 +4,7 @@ import type { BottomTabBarProps } from 'expo-router/js-tabs'; import { useEffect, useRef } from 'react'; import { Animated, Easing, Pressable, StyleSheet, View } from 'react-native'; import { colors } from '../../src/theme'; +import { useRole } from '../../src/context/RoleContext'; /** 无 index 路由,默认进入「岗位」Tab 作为落地页 */ export const unstable_settings = { @@ -161,6 +162,12 @@ function TabBar({ state, descriptors, navigation, insets }: BottomTabBarProps) { * 底部 Tab 导航:4 个 Tab(岗位/沟通/成长/我的),高级感极简风格 */ export default function TabLayout() { + const { setRole } = useRole(); + + useEffect(() => { + setRole('candidate'); + }, [setRole]); + return ( + + @@ -17,6 +19,6 @@ export default function RootLayout() { - + ); } \ No newline at end of file diff --git a/app/identity-select.tsx b/app/identity-select.tsx index f2c69b4..b2abf25 100644 --- a/app/identity-select.tsx +++ b/app/identity-select.tsx @@ -11,6 +11,7 @@ import { } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useIdentityOptions } from '../src/hooks'; +import { useRole } from '../src/context/RoleContext'; import { colors } from '../src/theme'; import type { IdentityOption } from '../src/types'; @@ -97,15 +98,18 @@ function ErrorState({ onRetry }: { onRetry: () => void }) { export default function IdentitySelectScreen() { const router = useRouter(); + const { setRole } = useRole(); const { options, loading, error, refresh } = useIdentityOptions(); const [selectedId, setSelectedId] = useState(null); const handleCandidatePress = () => { + setRole('candidate'); router.replace('/(tabs)'); }; const handleEnterprisePress = () => { - router.push('/enterprise-overview'); + setRole('enterprise'); + router.replace('/(enterprise)'); }; return ( diff --git a/src/context/RoleContext.tsx b/src/context/RoleContext.tsx new file mode 100644 index 0000000..c8da690 --- /dev/null +++ b/src/context/RoleContext.tsx @@ -0,0 +1,77 @@ +import { + createContext, + useContext, + useEffect, + useState, + type ReactNode, +} from 'react'; + +/* ===================== 角色(端)上下文 ===================== + * 区分「求职端 candidate」与「企业端 enterprise」两套管路。 + * - 进入 (tabs) 视为求职端,进入 (enterprise) 视为企业端; + * - identity-select 选定身份时写入 role,两端布局挂载时也会兜底校正。 + * + * 持久化:当前为会话内(内存)实现,App 重启后回到 null(重新选择身份)。 + * 若已安装 @react-native-async-storage/async-storage,可将文件底部 + * persistRole / loadRole 替换为真实读写,即可跨重启记住当前端。 + */ + +export type AppRole = 'candidate' | 'enterprise'; + +interface RoleContextValue { + role: AppRole | null; + setRole: (role: AppRole) => void; + clearRole: () => void; +} + +const RoleContext = createContext(undefined); + +let memoryRole: AppRole | null = null; + +function persistRole(role: AppRole | null): void { + memoryRole = role; + // TODO(持久化): 安装 async-storage 后改为 AsyncStorage.setItem('app_role', role ?? '') +} + +function loadRole(): Promise { + return Promise.resolve(memoryRole); + // TODO(持久化): 安装 async-storage 后改为 AsyncStorage.getItem('app_role') +} + +export function RoleProvider({ children }: { children: ReactNode }) { + const [role, setRoleState] = useState(null); + + useEffect(() => { + let active = true; + loadRole().then((stored) => { + if (active && stored) setRoleState(stored); + }); + return () => { + active = false; + }; + }, []); + + const setRole = (next: AppRole) => { + setRoleState(next); + persistRole(next); + }; + + const clearRole = () => { + setRoleState(null); + persistRole(null); + }; + + return ( + + {children} + + ); +} + +export function useRole(): RoleContextValue { + const ctx = useContext(RoleContext); + if (!ctx) { + throw new Error('useRole 必须在 RoleProvider 内部使用'); + } + return ctx; +}