Browse Source

refactor(menu): 按角色拆分功能菜单,从根上消除角色混淆

- 类型层:AppRole('candidate'|'enterprise') 下沉至 src/types/user.ts,
  RoleContext 改为复用并 re-export;types 桶导出补登记 AppRole
- 数据层:MOCK_MENU_ITEMS 拆为 MOCK_CANDIDATE_MENU_ITEMS(16 项求职者功能
  +企业入驻入口) 与 MOCK_ENTERPRISE_MENU_ITEMS(6 项企业功能)
- fetchMenuItems(role) 按角色下发对应菜单
- 企业端总览 EnterpriseOverview 新增 menuItems 字段,接入 MOCK_ENTERPRISE_MENU_ITEMS
- useProfileData(role) 透传角色;profile.tsx 用 useRole() 传入
- 企业端「我的」新增「更多服务」区渲染 menuItems(与「管理后台」分区并列)

效果:求职者端不再出现发布岗位/岗位管理等企业功能,企业端不再混排
求职者功能,角色隔离从根上解决后台术语/企业功能暴露给 C 端的问题
master
doublex001 2 months ago
parent
commit
0431d84ad3
  1. 32
      app/(enterprise)/enterprise-overview.tsx
  2. 4
      app/(tabs)/profile.tsx
  3. 34
      src/api/api.ts
  4. 4
      src/context/RoleContext.tsx
  5. 7
      src/hooks/useProfileData.ts
  6. 7
      src/types/enterprise.ts
  7. 1
      src/types/index.ts
  8. 8
      src/types/user.ts

32
app/(enterprise)/enterprise-overview.tsx

@ -12,7 +12,7 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { useEnterpriseOverview } from '../../src/hooks'; import { useEnterpriseOverview } from '../../src/hooks';
import { colors, headerGradient } from '../../src/theme'; import { colors, headerGradient } from '../../src/theme';
import type { EnterpriseOverview, EnterpriseQuickAction, EnterpriseStat, JobEvaluationSummary } from '../../src/types'; import type { EnterpriseOverview, EnterpriseQuickAction, EnterpriseStat, JobEvaluationSummary, MenuItem } from '../../src/types';
const COLOR_TOKENS: Record<string, string> = { const COLOR_TOKENS: Record<string, string> = {
primary: colors.primary, primary: colors.primary,
@ -194,6 +194,35 @@ function ManageList({ actions }: { actions: EnterpriseQuickAction[] }) {
); );
} }
/* ===================== 子组件:企业功能菜单 ===================== */
/**
* 企业端功能菜单(更多服务):发布岗位 / 岗位管理 / 人才推荐等业务功能。
* 与求职者端菜单严格隔离,由 AppRole='enterprise' 下发,避免角色混淆。
*/
function MenuList({ items }: { items: MenuItem[] }) {
const router = useRouter();
if (items.length === 0) return null;
return (
<View style={styles.card}>
<Text style={styles.standaloneTitle}>更多服务</Text>
{items.map((item) => (
<Pressable
key={item.id}
style={styles.manageRow}
onPress={() => item.route && router.push(item.route as any)}
>
<View style={[styles.manageIcon, { backgroundColor: colors.primary + '15' }]}>
<Ionicons name={item.icon as keyof typeof Ionicons.glyphMap} size={20} color={colors.primary} />
</View>
<Text style={styles.manageLabel}>{item.title}</Text>
<Ionicons name="chevron-forward" size={18} color={colors.textHint} />
</Pressable>
))}
</View>
);
}
/* ===================== 页面主组件 ===================== */ /* ===================== 页面主组件 ===================== */
export default function EnterpriseOverviewScreen() { export default function EnterpriseOverviewScreen() {
@ -229,6 +258,7 @@ export default function EnterpriseOverviewScreen() {
<QuickActions actions={overview.quickActions} /> <QuickActions actions={overview.quickActions} />
<JobEvaluationList jobs={overview.jobEvaluations} /> <JobEvaluationList jobs={overview.jobEvaluations} />
<ManageList actions={overview.manageActions} /> <ManageList actions={overview.manageActions} />
<MenuList items={overview.menuItems} />
</ScrollView> </ScrollView>
</View> </View>
); );

4
app/(tabs)/profile.tsx

@ -11,6 +11,7 @@ import {
} from 'react-native'; } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { useProfileData } from '../../src/hooks'; import { useProfileData } from '../../src/hooks';
import { useRole } from '../../src/context/RoleContext';
import { colors, headerGradientSolid } from '../../src/theme'; import { colors, headerGradientSolid } from '../../src/theme';
import type { Badge, DimensionScore, EnterpriseEntry, JobPreference, MenuItem, UserStats } from '../../src/types'; import type { Badge, DimensionScore, EnterpriseEntry, JobPreference, MenuItem, UserStats } from '../../src/types';
@ -166,6 +167,7 @@ function EnterpriseEntryCard({ entry }: { entry: EnterpriseEntry }) {
* 我的页 * 我的页
*/ */
export default function ProfileScreen() { export default function ProfileScreen() {
const { role } = useRole();
const { const {
profile, profile,
userStats, userStats,
@ -178,7 +180,7 @@ export default function ProfileScreen() {
loading, loading,
error, error,
refresh, refresh,
} = useProfileData(); } = useProfileData(role ?? 'candidate');
const router = useRouter(); const router = useRouter();
// 错误态 // 错误态

34
src/api/api.ts

@ -60,6 +60,7 @@ import type {
OnboardingChecklist, OnboardingChecklist,
JobProgress, JobProgress,
IdentityOption, IdentityOption,
AppRole,
AIInterviewData, AIInterviewData,
InterviewSession, InterviewSession,
HREvaluation, HREvaluation,
@ -609,8 +610,12 @@ const MOCK_USER_STATS: UserStats = {
offers: 3, offers: 3,
}; };
/** 功能菜单列表(设置类入口已移至设置页,这里只保留业务功能) */ /**
const MOCK_MENU_ITEMS: MenuItem[] = [ * 求职者端功能菜单
* 只含求职者相关功能;「企业入驻」作为切换到企业端的入口保留在此。
* 企业侧功能(发布岗位 / 岗位管理等)一律不在此下发,避免角色混淆。
*/
const MOCK_CANDIDATE_MENU_ITEMS: MenuItem[] = [
{ id: 'resume', title: '我的简历', icon: 'document-text-outline', route: '/resume' }, { id: 'resume', title: '我的简历', icon: 'document-text-outline', route: '/resume' },
{ id: 'favorites', title: '我的收藏', icon: 'heart-outline', route: '/favorites' }, { id: 'favorites', title: '我的收藏', icon: 'heart-outline', route: '/favorites' },
{ id: 'history', title: '浏览历史', icon: 'time-outline', route: '/history' }, { id: 'history', title: '浏览历史', icon: 'time-outline', route: '/history' },
@ -627,14 +632,22 @@ const MOCK_MENU_ITEMS: MenuItem[] = [
{ id: 'game', title: '通关小游戏', icon: 'trophy-outline', route: '/game' }, { id: 'game', title: '通关小游戏', icon: 'trophy-outline', route: '/game' },
{ id: 'legal', title: '阿布阿诺法律服务', icon: 'shield-checkmark-outline', route: '/legal' }, { id: 'legal', title: '阿布阿诺法律服务', icon: 'shield-checkmark-outline', route: '/legal' },
{ id: 'enterprise-certification', title: '企业入驻', icon: 'business-outline', route: '/enterprise-certification' }, { id: 'enterprise-certification', title: '企业入驻', icon: 'business-outline', route: '/enterprise-certification' },
];
/**
* 企业端功能菜单
* 只含企业 / HR 相关功能,在企业端「我的 - 更多服务」下发。
* 不设「候选人详情」菜单入口:该页需要候选人 id,独立入口会落到兜底数据,
* 正确路径是「人才推荐 → 列表点击 → 带 id 进详情」。
* 运营规则配置 / 数据审计等后台项见 MOCK_ENTERPRISE_MANAGE_ACTIONS。
*/
const MOCK_ENTERPRISE_MENU_ITEMS: MenuItem[] = [
{ id: 'job-publish', title: '发布岗位', icon: 'add-circle-outline', route: '/job-publish' }, { id: 'job-publish', title: '发布岗位', icon: 'add-circle-outline', route: '/job-publish' },
{ id: 'job-evaluations', title: '岗位评价管理', icon: 'chatbubbles-outline', route: '/job-evaluations' },
{ id: 'candidate-detail', title: '候选人详情', icon: 'person-outline', route: '/candidate-detail' },
{ id: 'interview-score', title: '面试评分', icon: 'create-outline', route: '/interview-score' },
{ id: 'job-management', title: '岗位管理', icon: 'business-outline', route: '/job-management' }, { id: 'job-management', title: '岗位管理', icon: 'business-outline', route: '/job-management' },
{ id: 'job-evaluations', title: '岗位评价管理', icon: 'chatbubbles-outline', route: '/job-evaluations' },
{ id: 'talent-recommendation', title: '人才推荐', icon: 'people-outline', route: '/talent-recommendation' }, { id: 'talent-recommendation', title: '人才推荐', icon: 'people-outline', route: '/talent-recommendation' },
{ id: 'interview-score', title: '面试评分', icon: 'create-outline', route: '/interview-score' },
{ id: 'enterprise-chat', title: '企业沟通', icon: 'chatbubble-outline', route: '/enterprise-chat' }, { id: 'enterprise-chat', title: '企业沟通', icon: 'chatbubble-outline', route: '/enterprise-chat' },
// 注:运营规则配置 / 数据审计报告属后台管理术语,已迁至企业端「我的-管理后台」,不在此暴露给求职者
]; ];
/** 企业端入口 */ /** 企业端入口 */
@ -1093,11 +1106,13 @@ export async function fetchUserStats(): Promise<UserStats> {
* 获取个人中心功能菜单列表 * 获取个人中心功能菜单列表
* @method GET * @method GET
* @url /api/user/menus * @url /api/user/menus
* @param role { AppRole } 当前角色(端),按角色下发不同菜单,避免求职者端出现企业功能、反之亦然
* @returns Promise<MenuItem[]> 菜单数组 * @returns Promise<MenuItem[]> 菜单数组
*/ */
export async function fetchMenuItems(): Promise<MenuItem[]> { export async function fetchMenuItems(role: AppRole = 'candidate'): Promise<MenuItem[]> {
// TODO: const res = await fetch(`${API_BASE_URL}/api/user/menus`, { method: 'GET' }); return res.json(); // TODO: const res = await fetch(`${API_BASE_URL}/api/user/menus?role=${role}`, { method: 'GET' }); return res.json();
return mockRequest(MOCK_MENU_ITEMS); const items = role === 'enterprise' ? MOCK_ENTERPRISE_MENU_ITEMS : MOCK_CANDIDATE_MENU_ITEMS;
return mockRequest(items);
} }
/** /**
@ -2396,6 +2411,7 @@ const MOCK_ENTERPRISE_OVERVIEW: EnterpriseOverview = {
jobEvaluations: MOCK_JOB_EVALUATION_SUMMARIES, jobEvaluations: MOCK_JOB_EVALUATION_SUMMARIES,
quickActions: MOCK_ENTERPRISE_QUICK_ACTIONS, quickActions: MOCK_ENTERPRISE_QUICK_ACTIONS,
manageActions: MOCK_ENTERPRISE_MANAGE_ACTIONS, manageActions: MOCK_ENTERPRISE_MANAGE_ACTIONS,
menuItems: MOCK_ENTERPRISE_MENU_ITEMS,
}; };
/** /**

4
src/context/RoleContext.tsx

@ -6,6 +6,7 @@ import {
type ReactNode, type ReactNode,
} from 'react'; } from 'react';
import { storageGet, storageSet, storageRemove } from '../lib/storage'; import { storageGet, storageSet, storageRemove } from '../lib/storage';
import type { AppRole } from '../types';
/* ===================== 角色(端)上下文 ===================== /* ===================== 角色(端)上下文 =====================
* 区分「求职端 candidate」与「企业端 enterprise」两套管路。 * 区分「求职端 candidate」与「企业端 enterprise」两套管路。
@ -17,7 +18,8 @@ import { storageGet, storageSet, storageRemove } from '../lib/storage';
* persistRole / loadRole 替换为真实读写,即可跨重启记住当前端。 * persistRole / loadRole 替换为真实读写,即可跨重启记住当前端。
*/ */
export type AppRole = 'candidate' | 'enterprise'; /** 角色类型统一由 types 层定义,此处 re-export 便于页面从 context 一并导入 */
export type { AppRole };
interface RoleContextValue { interface RoleContextValue {
role: AppRole | null; role: AppRole | null;

7
src/hooks/useProfileData.ts

@ -14,6 +14,7 @@ import {
fetchUserStats, fetchUserStats,
} from '../api'; } from '../api';
import type { import type {
AppRole,
Badge, Badge,
DimensionScore, DimensionScore,
EnterpriseEntry, EnterpriseEntry,
@ -37,7 +38,7 @@ interface ProfileData {
enterpriseEntry: EnterpriseEntry; enterpriseEntry: EnterpriseEntry;
} }
export function useProfileData() { export function useProfileData(role: AppRole = 'candidate') {
const fetcher = useCallback(async (): Promise<ProfileData> => { const fetcher = useCallback(async (): Promise<ProfileData> => {
const [profile, userStats, overallScore, dimensions, preference, badges, menuItems, enterpriseEntry] = await Promise.all([ const [profile, userStats, overallScore, dimensions, preference, badges, menuItems, enterpriseEntry] = await Promise.all([
fetchUserProfile(), fetchUserProfile(),
@ -46,11 +47,11 @@ export function useProfileData() {
fetchDimensionScores(), fetchDimensionScores(),
fetchJobPreference(), fetchJobPreference(),
fetchBadges(), fetchBadges(),
fetchMenuItems(), fetchMenuItems(role),
fetchEnterpriseEntry(), fetchEnterpriseEntry(),
]); ]);
return { profile, userStats, overallScore, dimensions, preference, badges, menuItems, enterpriseEntry }; return { profile, userStats, overallScore, dimensions, preference, badges, menuItems, enterpriseEntry };
}, []); }, [role]);
const { data, loading, error, refresh } = useAsyncData(fetcher); const { data, loading, error, refresh } = useAsyncData(fetcher);

7
src/types/enterprise.ts

@ -8,6 +8,7 @@
* - 候选人详情 * - 候选人详情
* - 面试评分 * - 面试评分
*/ */
import type { MenuItem } from './user';
/* ===================== 企业数据总览 ===================== */ /* ===================== 企业数据总览 ===================== */
@ -70,6 +71,12 @@ export interface EnterpriseOverview {
* 仅企业端「我的」展示,不在求职者端菜单暴露,避免后台术语干扰 C 端用户 * 仅企业端「我的」展示,不在求职者端菜单暴露,避免后台术语干扰 C 端用户
*/ */
manageActions: EnterpriseQuickAction[]; manageActions: EnterpriseQuickAction[];
/**
* 企业端功能菜单(发布岗位、岗位管理、人才推荐等业务功能)
* 由 AppRole='enterprise' 下发,仅在企业端「我的 - 更多服务」展示,
* 与求职者端菜单严格隔离,避免角色混淆
*/
menuItems: MenuItem[];
} }
/** 企业总览快捷入口 */ /** 企业总览快捷入口 */

1
src/types/index.ts

@ -117,6 +117,7 @@ export type {
TopCandidate, TopCandidate,
} from './rank'; } from './rank';
export type { export type {
AppRole,
Badge, Badge,
DimensionScore, DimensionScore,
EnterpriseEntry, EnterpriseEntry,

8
src/types/user.ts

@ -25,6 +25,14 @@ export interface UserStats {
offers: number; offers: number;
} }
/**
* 应用角色
* - candidate:求职者
* - enterprise:企业 / HR
* 用于按角色下发不同的功能菜单,避免求职者端出现企业功能、反之亦然
*/
export type AppRole = 'candidate' | 'enterprise';
/** 功能菜单项 */ /** 功能菜单项 */
export interface MenuItem { export interface MenuItem {
id: string; id: string;

Loading…
Cancel
Save