diff --git a/AGENTS.md b/AGENTS.md index e52ca11..e6e6406 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,9 +10,9 @@ Read the exact versioned docs at https://docs.expo.dev/versions/v57.0.0/ before 【核心原则】 做任务前,先检查问题有没有错误前提、逻辑跳跃和信息缺失。 不要迎合我,要独立判断,区分事实、预测和主观观点。 涉及数字、人物和结论时尽量核实来源。 不同意就直接指出并给出依据、风险和替代解释。 还要主动提醒我忽略的变量、成本和偏差。 -请在每周的周一-周五17:50提交一次git仓库,说明今天改动的内容 -业务逻辑分层管理,不是没有,而是被妥善组织: +请在每周的周一-周五的每天17:50提交一次git仓库,说明今天新增与改动的内容,每天开始的时候拉取一次,每天结束的时候提交一次,上班时间是9:00-18:00 +业务逻辑分层管理,不是没有,而是被妥善组织: +------------------+--------------------------------------------------+----------------------------------------------------------------------------------------+ | 层级 | 职责 | 业务逻辑示例 | +------------------+--------------------------------------------------+----------------------------------------------------------------------------------------+ diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 7211454..b557101 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,62 +1,178 @@ -import { Tabs } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; -import type { ComponentProps } from 'react'; -import { StyleSheet, Text } from 'react-native'; -import type { ColorValue } from 'react-native'; +import { BlurView } from 'expo-blur'; +import { Tabs } from 'expo-router/js-tabs'; +import type { BottomTabBarProps } from 'expo-router/js-tabs'; +import { useEffect, useRef } from 'react'; +import type { ComponentProps, ReactNode } from 'react'; +import { Animated, Pressable, StyleSheet, Text, View, type ColorValue } from 'react-native'; import { colors } from '../../src/theme'; type IconName = ComponentProps['name']; +type TabIconProps = { focused: boolean; color: ColorValue; size: number }; + +/** 内容底部预留高度:悬浮 Tab 栏高度 + 悬浮间距 + 余量 */ +const TAB_BAR_FLOAT_SPACE = 84; /** - * 根据选中状态返回实心/描边图标 + * 选中/未选中切换实心与描边图标 */ function tabIcon(filled: IconName, outline: IconName) { - return ({ color, size, focused }: { color: ColorValue; size: number; focused: boolean }) => ( + return ({ color, size, focused }: TabIconProps) => ( ); } +/** 单个 Tab 项:选中时图标轻弹 + 浅蓝胶囊高亮 */ +function TabItem({ + focused, + label, + icon, + onPress, + onLongPress, +}: { + focused: boolean; + label: string; + icon?: (props: TabIconProps) => ReactNode; + onPress: () => void; + onLongPress: () => void; +}) { + // 图标弹跳动画:选中 1.15,未选中回到 1 + const scale = useRef(new Animated.Value(1)).current; + useEffect(() => { + Animated.spring(scale, { + toValue: focused ? 1.15 : 1, + useNativeDriver: true, + friction: 6, + tension: 200, + }).start(); + }, [focused, scale]); + + const color = focused ? colors.primary : colors.textSecondary; + return ( + + + + {icon?.({ focused, color, size: 22 })} + + {label} + + + ); +} + +/** 悬浮毛玻璃 Tab 栏 */ +function FloatingTabBar({ 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 导航:首页默认选中,蓝色高亮 + * 底部 Tab 导航:悬浮毛玻璃胶囊 + 选中高亮 + 图标弹跳 */ export default function TabLayout() { return ( ( - {children} - ), + // 悬浮 Tab 栏不占布局空间,给每个页面内容底部预留空间,避免被遮挡 + sceneStyle: { paddingBottom: TAB_BAR_FLOAT_SPACE }, }} + tabBar={(props) => } > - - - - - + + + + + ); } const styles = StyleSheet.create({ - labelText: { + tabBarWrap: { + position: 'absolute', + left: 16, + right: 16, + bottom: 0, + }, + // 外层负责 iOS 阴影(不裁剪,阴影在圆角外正常显示) + tabBarShadow: { + borderRadius: 24, + shadowColor: colors.textPrimary, + shadowOffset: { width: 0, height: 6 }, + shadowOpacity: 0.15, + shadowRadius: 16, + }, + // 内层 BlurView:真毛玻璃(iOS)/ 半透明(Android 31 以下降级)+ Android 阴影 + tabBar: { + flexDirection: 'row', + alignItems: 'center', + borderRadius: 24, + paddingHorizontal: 6, + paddingVertical: 8, + overflow: 'hidden', + elevation: 10, + }, + tabItem: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + tabItemContent: { + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 18, + }, + tabItemContentActive: { + backgroundColor: colors.primaryLight, + }, + tabLabel: { + marginTop: 4, fontSize: 11, + fontWeight: '600', }, -}); \ No newline at end of file +}); diff --git a/app/(tabs)/jobs.tsx b/app/(tabs)/jobs.tsx index 020a894..8344f86 100644 --- a/app/(tabs)/jobs.tsx +++ b/app/(tabs)/jobs.tsx @@ -1,4 +1,5 @@ import { Ionicons } from '@expo/vector-icons'; +import { useRouter } from 'expo-router'; import { useState } from 'react'; import { ActivityIndicator, @@ -94,10 +95,14 @@ function FilterGroup({ ); } -/** 单张岗位卡片 */ +/** 单张岗位卡片(整卡可点击进入详情) */ function JobCard({ job }: { job: Job }) { + const router = useRouter(); return ( - + [styles.card, pressed && styles.cardPressed]} + onPress={() => router.push(`/jobs/${job.id}`)} + > {/* 公司名 + 地点 */} {job.companyName} @@ -117,13 +122,7 @@ function JobCard({ job }: { job: Job }) { {job.score}分 · {JOB_LEVEL_LABEL[job.level]} - { - console.log('查看详情', job.id, job.positionName); - }} - > - 查看详情 > - + 查看详情 > {/* 福利标签 */} @@ -134,7 +133,7 @@ function JobCard({ job }: { job: Job }) { ))} - + ); } @@ -529,6 +528,9 @@ const styles = StyleSheet.create({ shadowRadius: 8, elevation: 3, }, + cardPressed: { + opacity: 0.85, + }, cardHeader: { flexDirection: 'row', alignItems: 'center', diff --git a/app/(tabs)/profile.tsx b/app/(tabs)/profile.tsx index 83b3cf5..844cacd 100644 --- a/app/(tabs)/profile.tsx +++ b/app/(tabs)/profile.tsx @@ -1,4 +1,5 @@ import { Ionicons } from '@expo/vector-icons'; +import { LinearGradient } from 'expo-linear-gradient'; import { useRouter } from 'expo-router'; import { ActivityIndicator, @@ -15,13 +16,13 @@ import type { Badge, DimensionScore, JobPreference } from '../../src/types'; /* ===================== 展示层映射(全部使用主题令牌) ===================== */ -/** 维度等级 → 展示配置(标签文案 + 配色) */ +/** 维度等级 → 展示配置(优秀=品牌蓝,良好=灰色) */ const DIMENSION_LEVEL: Record< DimensionScore['level'], - { label: string; bg: string; text: string } + { label: string; bar: string; bg: string; text: string } > = { - excellent: { label: '优秀', bg: colors.successLight, text: colors.successText }, - good: { label: '良好', bg: colors.primaryLight, text: colors.primaryText }, + excellent: { label: '优秀', bar: colors.primary, bg: colors.primaryLight, text: colors.primaryDark }, + good: { label: '良好', bar: colors.textHint, bg: colors.pageBg, text: colors.textSecondary }, }; /** 求职偏好字段 → 展示标签 */ @@ -35,29 +36,53 @@ const PREF_ITEMS: { key: keyof JobPreference; label: string }[] = [ /* ===================== 子组件 ===================== */ -/** 单个维度评分项 */ -function DimensionItem({ dimension }: { dimension: DimensionScore }) { +/** 单个维度评分行(横向评分条) */ +function DimensionRow({ dimension }: { dimension: DimensionScore }) { const theme = DIMENSION_LEVEL[dimension.level]; return ( - + {dimension.name} - - {dimension.score} - - {theme.label} - + + + + {dimension.score} + + {theme.label} ); } -/** 单个勋章卡片 */ +/** 单个勋章卡片(已获得=彩色,未获得=灰色+进度条) */ function BadgeCard({ badge }: { badge: Badge }) { return ( - {badge.icon} - {badge.title} + + {badge.icon} + + + {badge.title} + {badge.description} + {badge.obtained ? ( + + 已获得 + + ) : ( + <> + + + + + {badge.progress}/{badge.target} + + + )} ); } @@ -99,29 +124,36 @@ export default function ProfileScreen() { - {/* 1. 顶部导航 */} - - 我的 - router.push('/settings')}> - - - - - {/* 2. 用户信息 */} - - + {/* 顶部渐变头部:标题 + 用户信息(随页面滚动) */} + + + 我的 + router.push('/settings')} hitSlop={8}> + + + + {profile.avatar} - {profile.name} + + {profile.name} + {profile.bio} + - {profile.bio} - + - {/* 3. 综合评分卡片 */} + {/* 内容区 */} + + {/* 综合评分卡片 */} {overallScore.score} @@ -130,49 +162,60 @@ export default function ProfileScreen() { {overallScore.level} - - {overallScore.rank} - console.log('评分申诉')}> - 评分申诉 - - + {overallScore.rank} - {/* 4. 八大维度评分 */} - 八大维度评分 - + {/* 八大维度评分 */} + + + 八大维度评分 + console.log('评分申诉')} hitSlop={6}> + 评分申诉 + + + {dimensions.map((dimension) => ( - + ))} - {/* 5. 求职偏好 */} - 求职偏好 + {/* 求职偏好 */} + + + 求职偏好 + console.log('编辑求职偏好')} hitSlop={6}> + 编辑 + + {PREF_ITEMS.map(({ key, label }, index) => { const value = preference[key]; const isLast = index === PREF_ITEMS.length - 1; + const isStatus = key === 'status'; return ( - + console.log(`编辑${label}`)} + > {label} - {key === 'status' ? ( - + {isStatus ? ( + + {value} ) : ( {value} )} - + ); })} - {/* 6. 全部勋章 */} + {/* 全部勋章 */} - 全部勋章 - console.log('查看全部勋章')}> - > - + + 全部勋章 ))} + ); @@ -225,92 +269,91 @@ const styles = StyleSheet.create({ fontWeight: '600', color: colors.white, }, - scroll: { - flex: 1, - }, - content: { + /* ---- 顶部渐变头部 ---- */ + headerGradient: { paddingHorizontal: 20, + paddingTop: 8, paddingBottom: 24, + borderBottomLeftRadius: 24, + borderBottomRightRadius: 24, }, - /* ---- 顶部导航 ---- */ - header: { - height: 48, + headerRow: { + height: 40, flexDirection: 'row', alignItems: 'center', - justifyContent: 'center', + justifyContent: 'space-between', }, headerTitle: { fontSize: 17, fontWeight: '700', - color: colors.textPrimary, - }, - headerAction: { - position: 'absolute', - right: 0, - width: 40, - height: 40, - alignItems: 'center', - justifyContent: 'center', - }, - /* ---- 用户信息 ---- */ - profileCard: { - marginTop: 8, - backgroundColor: colors.white, - borderRadius: 16, - padding: 20, - ...shadow, + color: colors.white, }, - profileTop: { + profileRow: { flexDirection: 'row', alignItems: 'center', + marginTop: 8, }, avatar: { - width: 56, - height: 56, - borderRadius: 28, - backgroundColor: colors.primary, + width: 60, + height: 60, + borderRadius: 30, + backgroundColor: colors.white, alignItems: 'center', justifyContent: 'center', }, avatarText: { - fontSize: 24, + fontSize: 26, fontWeight: '700', - color: colors.white, + color: colors.primary, }, - userName: { + profileInfo: { marginLeft: 14, + }, + userName: { fontSize: 20, fontWeight: '700', - color: colors.textPrimary, + color: colors.white, }, bio: { - marginTop: 14, + marginTop: 6, fontSize: 13, - color: colors.textSecondary, + color: colors.white, + opacity: 0.85, + }, + /* ---- 内容区 ---- */ + scroll: { + flex: 1, + }, + scrollContent: { + paddingBottom: 24, + }, + content: { + paddingHorizontal: 20, + paddingTop: 16, }, /* ---- 综合评分卡片 ---- */ scoreCard: { flexDirection: 'row', alignItems: 'center', - marginTop: 12, backgroundColor: colors.white, borderRadius: 16, - padding: 20, + paddingHorizontal: 20, + paddingVertical: 20, ...shadow, }, scoreValue: { fontSize: 40, fontWeight: '700', - color: colors.primary, + color: colors.primaryDark, }, scoreUnit: { fontSize: 16, fontWeight: '600', - color: colors.primary, + color: colors.primaryDark, }, scoreTag: { marginLeft: 12, - backgroundColor: colors.successLight, + backgroundColor: colors.primaryLight, paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12, @@ -318,80 +361,85 @@ const styles = StyleSheet.create({ scoreTagText: { fontSize: 13, fontWeight: '600', - color: colors.successText, + color: colors.primaryDark, }, - scoreRight: { + scoreRank: { flex: 1, - alignItems: 'flex-end', + textAlign: 'right', marginLeft: 12, - }, - scoreRank: { fontSize: 13, color: colors.textSecondary, }, - scoreAppeal: { - marginTop: 8, - fontSize: 13, - color: colors.primary, - }, /* ---- 分区标题 ---- */ - sectionTitle: { - fontSize: 18, - fontWeight: '700', - color: colors.textPrimary, - }, - sectionGap: { - marginTop: 24, - marginBottom: 12, - }, sectionHeader: { flexDirection: 'row', alignItems: 'center', - justifyContent: 'space-between', marginTop: 24, marginBottom: 12, }, - sectionMore: { + sectionBar: { + width: 4, + height: 16, + borderRadius: 2, + backgroundColor: colors.primary, + marginRight: 8, + }, + sectionTitle: { fontSize: 18, fontWeight: '700', - color: colors.textSecondary, + color: colors.textPrimary, }, - /* ---- 八大维度 ---- */ - dimGrid: { - flexDirection: 'row', - flexWrap: 'wrap', - justifyContent: 'space-between', + sectionTitleGrow: { + flex: 1, + }, + sectionLink: { + fontSize: 13, + color: colors.primary, }, - dimItem: { - width: '48.5%', + /* ---- 八大维度(横向评分条) ---- */ + dimCard: { backgroundColor: colors.white, borderRadius: 16, - padding: 16, - marginBottom: 12, + paddingHorizontal: 16, + paddingVertical: 6, ...shadow, }, + dimRow: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 12, + }, dimName: { + width: 78, fontSize: 13, color: colors.textSecondary, }, - dimScoreRow: { - flexDirection: 'row', - alignItems: 'center', - marginTop: 8, + dimTrack: { + flex: 1, + height: 6, + borderRadius: 3, + backgroundColor: colors.border, + marginHorizontal: 12, + }, + dimFill: { + height: 6, + borderRadius: 3, }, dimScore: { - fontSize: 24, + width: 28, + textAlign: 'right', + fontSize: 14, fontWeight: '700', color: colors.textPrimary, }, levelTag: { - marginLeft: 8, + marginLeft: 10, paddingHorizontal: 8, - paddingVertical: 3, + paddingVertical: 2, borderRadius: 10, }, levelTagText: { - fontSize: 12, + fontSize: 11, fontWeight: '600', }, /* ---- 求职偏好 ---- */ @@ -422,14 +470,19 @@ const styles = StyleSheet.create({ fontWeight: '600', color: colors.textPrimary, }, - prefStatusTag: { - backgroundColor: colors.successLight, - paddingHorizontal: 10, - paddingVertical: 4, - borderRadius: 12, + prefStatus: { + flexDirection: 'row', + alignItems: 'center', + }, + prefStatusDot: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: colors.success, + marginRight: 6, }, prefStatusText: { - fontSize: 13, + fontSize: 14, fontWeight: '600', color: colors.successText, }, @@ -453,16 +506,51 @@ const styles = StyleSheet.create({ badgeIcon: { fontSize: 32, }, + badgeIconLocked: { + opacity: 0.35, + }, badgeTitle: { marginTop: 10, fontSize: 14, fontWeight: '700', color: colors.textPrimary, }, + badgeTitleLocked: { + color: colors.textHint, + }, badgeDesc: { marginTop: 4, fontSize: 12, color: colors.textSecondary, textAlign: 'center', }, + badgeObtainedTag: { + marginTop: 10, + backgroundColor: colors.successLight, + paddingHorizontal: 10, + paddingVertical: 3, + borderRadius: 10, + }, + badgeObtainedText: { + fontSize: 11, + fontWeight: '600', + color: colors.successText, + }, + badgeProgressTrack: { + width: '100%', + height: 4, + borderRadius: 2, + backgroundColor: colors.border, + marginTop: 12, + }, + badgeProgressFill: { + height: 4, + borderRadius: 2, + backgroundColor: colors.primary, + }, + badgeProgressText: { + marginTop: 6, + fontSize: 11, + color: colors.textHint, + }, }); diff --git a/app/_layout.tsx b/app/_layout.tsx index 67d8858..d0a2332 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -10,6 +10,7 @@ export default function RootLayout() { + diff --git a/app/jobs/[id].tsx b/app/jobs/[id].tsx new file mode 100644 index 0000000..70c27cc --- /dev/null +++ b/app/jobs/[id].tsx @@ -0,0 +1,611 @@ +import { Ionicons } from '@expo/vector-icons'; +import { useLocalSearchParams, useRouter } from 'expo-router'; +import { useState } from 'react'; +import { + ActivityIndicator, + Pressable, + ScrollView, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import Svg, { Circle, Line, Polygon, Text as SvgText } from 'react-native-svg'; +import { useJobDetail } from '../../src/hooks'; +import { colors } from '../../src/theme'; +import { formatThousands } from '../../src/utils'; +import type { JobDimensionScore } from '../../src/types'; + +/* ===================== 雷达图(6 维度,SVG 绘制) ===================== */ + +/** 六边形雷达图:同心网格 + 轴线 + 数据多边形 + 维度标签 */ +function RadarChart({ + dimensions, + size = 160, +}: { + dimensions: JobDimensionScore[]; + size?: number; +}) { + const cx = size / 2; + const cy = size / 2; + const count = dimensions.length; + // 数据/网格最大半径(预留标签空间) + const R = size / 2 - 36; + const labelR = R + 16; + const levels = [0.25, 0.5, 0.75, 1]; + const angleFor = (i: number) => (-90 + i * (360 / count)) * (Math.PI / 180); + const pointFor = (i: number, ratio: number) => ({ + x: cx + R * ratio * Math.cos(angleFor(i)), + y: cy + R * ratio * Math.sin(angleFor(i)), + }); + const polygonPoints = (ratio: number) => + dimensions + .map((_, i) => { + const p = pointFor(i, ratio); + return `${p.x},${p.y}`; + }) + .join(' '); + + const dataPoints = dimensions.map((d, i) => pointFor(i, d.value / 100)); + + return ( + + {/* 同心网格 */} + {levels.map((ratio) => ( + + ))} + {/* 轴线 */} + {dimensions.map((_, i) => { + const p = pointFor(i, 1); + return ( + + ); + })} + {/* 数据多边形 */} + `${p.x},${p.y}`).join(' ')} + fill={colors.primary} + fillOpacity={0.15} + stroke={colors.primary} + strokeWidth={2} + /> + {/* 数据点 */} + {dataPoints.map((p, i) => ( + + ))} + {/* 维度标签 */} + {dimensions.map((d, i) => { + const lx = cx + labelR * Math.cos(angleFor(i)); + const ly = cy + labelR * Math.sin(angleFor(i)); + return ( + + {d.name} + + ); + })} + + ); +} + +/* ===================== 子组件 ===================== */ + +/** 顶部导航:返回 + 标题 + 分享 */ +function Header({ onBack }: { onBack: () => void }) { + return ( + + + + + 岗位详情 + console.log('分享')} hitSlop={8}> + + + + ); +} + +/** 分区标题:竖条装饰 + 标题 + 可选右侧链接 */ +function SectionTitle({ + title, + link, + onLink, +}: { + title: string; + link?: string; + onLink?: () => void; +}) { + return ( + + + {title} + {link ? ( + + {link} > + + ) : null} + + ); +} + +/* ===================== 页面 ===================== */ + +export default function JobDetailScreen() { + const { id } = useLocalSearchParams<{ id: string }>(); + const router = useRouter(); + const { detail, overallScore, salaryData, reviews, loading, error, refresh } = useJobDetail(id); + const [comment, setComment] = useState(''); + + // 错误态 + if (error) { + return ( + +
router.back()} /> + + 加载失败,请重试 + + 重试 + + + + ); + } + + // 加载态 + if (loading || !detail || !overallScore || !salaryData) { + return ( + +
router.back()} /> + + + + + ); + } + + return ( + +
router.back()} /> + + + {/* 1. 岗位基本信息 */} + + + {detail.positionName} + + + {detail.status} + + 浏览{formatThousands(detail.views)} + + + + {detail.salaryMin}-{detail.salaryMax}K · {detail.salaryMonths}薪 + + + {detail.companyName} · {detail.industry} · {detail.companySize} + + + {detail.companyLocation} · {detail.experience} · {detail.education} + + + {detail.benefits.map((item) => ( + + {item} + + ))} + + + + {/* 2. 岗位综合评分 */} + console.log('评分说明')} /> + + + + + + {overallScore.totalScore} + 分 + + + {overallScore.level} + + {overallScore.rank} + + + console.log('加入对比')} hitSlop={6}> + + 加入对比 + + + + {/* 3. 真实薪资区间 */} + console.log('全部')} /> + + {salaryData.ranges.map((range, index) => ( + + {range.experience} + + {range.min}-{range.max}K + + + ))} + {salaryData.dataSource} + + + {/* 4. 员工评价 */} + + {reviews.map((review) => ( + + + {review.userName} + {review.userRole} + {review.time} + + {review.content} + + {review.tags.map((tag) => ( + + {tag} + + ))} + + + ))} + + + {/* 5. 评论输入框(固定底部) */} + + + console.log('发布', comment)}> + 发布 + + + + ); +} + +// 跨平台阴影 +const shadow = { + shadowColor: colors.textPrimary, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.06, + shadowRadius: 8, + elevation: 3, +}; + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + backgroundColor: colors.pageBg, + }, + stateWrap: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + 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, + }, + /* ---- 顶部导航 ---- */ + header: { + height: 48, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + headerTitle: { + fontSize: 17, + fontWeight: '700', + color: colors.textPrimary, + }, + headerBtn: { + position: 'absolute', + top: 0, + width: 48, + height: 48, + alignItems: 'center', + justifyContent: 'center', + }, + scroll: { + flex: 1, + }, + content: { + paddingHorizontal: 20, + paddingBottom: 24, + }, + card: { + backgroundColor: colors.white, + borderRadius: 16, + padding: 16, + marginBottom: 16, + ...shadow, + }, + /* ---- 基本信息 ---- */ + basicTop: { + flexDirection: 'row', + alignItems: 'flex-start', + justifyContent: 'space-between', + }, + positionName: { + flex: 1, + fontSize: 22, + fontWeight: '700', + color: colors.textPrimary, + }, + statusCol: { + alignItems: 'flex-end', + marginLeft: 12, + }, + statusTag: { + backgroundColor: colors.successLight, + paddingHorizontal: 8, + paddingVertical: 3, + borderRadius: 10, + }, + statusText: { + fontSize: 12, + fontWeight: '600', + color: colors.successText, + }, + viewsText: { + marginTop: 6, + fontSize: 12, + color: colors.textHint, + }, + salary: { + marginTop: 14, + fontSize: 20, + fontWeight: '700', + color: colors.primary, + }, + metaText: { + marginTop: 8, + fontSize: 13, + color: colors.textSecondary, + }, + benefits: { + flexDirection: 'row', + flexWrap: 'wrap', + marginTop: 12, + }, + benefitTag: { + backgroundColor: colors.primaryLight, + borderWidth: 1, + borderColor: colors.primaryBorder, + borderRadius: 12, + paddingHorizontal: 10, + paddingVertical: 4, + marginRight: 8, + marginBottom: 8, + }, + benefitText: { + fontSize: 12, + color: colors.primary, + }, + /* ---- 分区标题 ---- */ + sectionHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 12, + }, + sectionBar: { + width: 4, + height: 16, + borderRadius: 2, + backgroundColor: colors.primary, + marginRight: 8, + }, + sectionTitle: { + flex: 1, + fontSize: 17, + fontWeight: '700', + color: colors.textPrimary, + }, + sectionLinkBtn: { + paddingLeft: 8, + }, + sectionLink: { + fontSize: 13, + color: colors.primary, + }, + /* ---- 综合评分 ---- */ + scoreRow: { + flexDirection: 'row', + alignItems: 'center', + }, + scoreInfo: { + flex: 1, + alignItems: 'flex-start', + marginLeft: 8, + }, + scoreNumberRow: { + flexDirection: 'row', + alignItems: 'baseline', + }, + scoreNumber: { + fontSize: 40, + fontWeight: '700', + color: colors.primary, + }, + scoreUnit: { + fontSize: 14, + fontWeight: '600', + color: colors.primary, + }, + levelTag: { + marginTop: 4, + backgroundColor: colors.successLight, + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 12, + }, + levelTagText: { + fontSize: 13, + fontWeight: '600', + color: colors.successText, + }, + scoreRank: { + marginTop: 8, + fontSize: 12, + color: colors.textSecondary, + }, + compareBtn: { + flexDirection: 'row', + alignItems: 'center', + alignSelf: 'flex-start', + marginTop: 16, + }, + compareText: { + marginLeft: 4, + fontSize: 14, + fontWeight: '600', + color: colors.primary, + }, + /* ---- 薪资区间 ---- */ + salaryRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 12, + borderBottomWidth: 1, + borderBottomColor: colors.borderLight, + }, + salaryRowLast: { + borderBottomWidth: 0, + }, + salaryExp: { + fontSize: 14, + color: colors.textSecondary, + }, + salaryRange: { + fontSize: 15, + fontWeight: '700', + color: colors.textPrimary, + }, + salarySource: { + marginTop: 12, + fontSize: 12, + color: colors.textHint, + }, + /* ---- 员工评价 ---- */ + reviewHeader: { + flexDirection: 'row', + alignItems: 'center', + }, + reviewName: { + fontSize: 14, + fontWeight: '700', + color: colors.textPrimary, + }, + reviewRole: { + marginLeft: 8, + fontSize: 12, + color: colors.textSecondary, + }, + reviewTime: { + flex: 1, + textAlign: 'right', + fontSize: 12, + color: colors.textHint, + }, + reviewContent: { + marginTop: 10, + fontSize: 13, + lineHeight: 20, + color: colors.textPrimary, + }, + reviewTags: { + flexDirection: 'row', + flexWrap: 'wrap', + marginTop: 10, + }, + reviewTag: { + backgroundColor: colors.pageBg, + borderWidth: 1, + borderColor: colors.border, + borderRadius: 12, + paddingHorizontal: 10, + paddingVertical: 4, + marginRight: 8, + marginBottom: 8, + }, + reviewTagText: { + fontSize: 12, + color: colors.textSecondary, + }, + /* ---- 评论输入框 ---- */ + commentBar: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 10, + backgroundColor: colors.white, + borderTopWidth: 1, + borderTopColor: colors.borderLight, + }, + commentInput: { + flex: 1, + height: 40, + borderRadius: 20, + backgroundColor: colors.pageBg, + paddingHorizontal: 16, + fontSize: 14, + color: colors.textPrimary, + }, + publishBtn: { + marginLeft: 12, + paddingHorizontal: 18, + height: 40, + borderRadius: 20, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.primaryBtn, + }, + publishText: { + fontSize: 14, + fontWeight: '600', + color: colors.white, + }, +}); diff --git a/package-lock.json b/package-lock.json index 1dfcd6a..4b5e9b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@expo/vector-icons": "^15.0.2", "expo": "~57.0.14", + "expo-blur": "~57.0.2", "expo-constants": "~57.0.12", "expo-font": "~57.0.1", "expo-linear-gradient": "~57.0.1", @@ -552,6 +553,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-async-generator-functions": { "version": "7.29.7", "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", @@ -925,6 +942,38 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-pure-annotations": { "version": "7.29.7", "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz", @@ -941,6 +990,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.8.tgz", + "integrity": "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-runtime": { "version": "7.29.7", "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz", @@ -970,6 +1035,38 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-typescript": { "version": "7.29.7", "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", @@ -1985,6 +2082,54 @@ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, + "node_modules/@react-native/babel-preset": { + "version": "0.86.2", + "resolved": "https://registry.npmmirror.com/@react-native/babel-preset/-/babel-preset-0.86.2.tgz", + "integrity": "sha512-4XKEJ6jKW9lXMB1O5o47gBoGgolde1fbX13gLW/erlcn+1ky+MHHo5UjuM3RWGdPJHIvIzekDDumSUHhB9x5iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@react-native/babel-plugin-codegen": "0.86.2", + "babel-plugin-syntax-hermes-parser": "0.36.0", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, "node_modules/@react-native/codegen": { "version": "0.86.2", "resolved": "https://registry.npmmirror.com/@react-native/codegen/-/codegen-0.86.2.tgz", @@ -2115,6 +2260,58 @@ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, + "node_modules/@react-native/metro-babel-transformer": { + "version": "0.86.2", + "resolved": "https://registry.npmmirror.com/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.86.2.tgz", + "integrity": "sha512-mX1wgLErdb2hDgXJr9zM9SWLe+ZteZTFTwRWGOQ53yEJwc9DVSnxdTlAtVdMeOj2ntycKh0R8jYdat2Am43cwQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@react-native/babel-preset": "0.86.2", + "hermes-parser": "0.36.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-estree": { + "version": "0.36.0", + "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.36.0.tgz", + "integrity": "sha512-A1+8zn5oss2CFP7pKsOaxorQG6FNIz1WU1VDqruLPPZl3LVgeE2C5xfFg8Ow6/Ow4mSslLLtYP1J3n38eKyW9w==", + "license": "MIT", + "peer": true + }, + "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-parser": { + "version": "0.36.0", + "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.36.0.tgz", + "integrity": "sha512-GdpwMmH5x6IpC1cijvcvYnlPB60Mh6kTSF/NFdYV/j56gYdi+0RIakYs+eqOV+bbO0SW7mgVVGSsTJxyPQfo3w==", + "license": "MIT", + "peer": true, + "dependencies": { + "hermes-estree": "0.36.0" + } + }, + "node_modules/@react-native/metro-config": { + "version": "0.86.2", + "resolved": "https://registry.npmmirror.com/@react-native/metro-config/-/metro-config-0.86.2.tgz", + "integrity": "sha512-hJno256j+MS0b3JD1aD3ouTGZVacKNVBuXL2atMQQ8BZ060vl1ptnZ83y569aDW+/rgFSOcqn6ydKeSz4uUKQQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@react-native/js-polyfills": "0.86.2", + "@react-native/metro-babel-transformer": "0.86.2", + "metro-config": "^0.84.3", + "metro-runtime": "^0.84.3" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, "node_modules/@react-native/normalize-colors": { "version": "0.86.2", "resolved": "https://registry.npmmirror.com/@react-native/normalize-colors/-/normalize-colors-0.86.2.tgz", @@ -2150,6 +2347,61 @@ "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", "license": "MIT" }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT", + "peer": true + }, "node_modules/@testing-library/jest-dom": { "version": "6.10.0", "resolved": "https://registry.npmmirror.com/@testing-library/jest-dom/-/jest-dom-6.10.0.tgz", @@ -2191,6 +2443,13 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "license": "MIT", + "peer": true + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmmirror.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2228,12 +2487,21 @@ "version": "19.2.18", "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.18.tgz", "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" } }, + "node_modules/@types/react-test-renderer": { + "version": "19.1.0", + "resolved": "https://registry.npmmirror.com/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz", + "integrity": "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmmirror.com/@types/yargs/-/yargs-17.0.35.tgz", @@ -3040,7 +3308,6 @@ "version": "3.2.3", "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -3139,6 +3406,13 @@ "integrity": "sha512-Ndrrf6BMPalkQPd/zubL+4YghH2J9NspapQ09uDXwYbvOPkP0oaqf5CkcwJ0b50kS2O3ul6yVu+jz+RY62Cejg==", "license": "MIT" }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "license": "MIT", + "peer": true + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -3358,6 +3632,17 @@ } } }, + "node_modules/expo-blur": { + "version": "57.0.2", + "resolved": "https://registry.npmmirror.com/expo-blur/-/expo-blur-57.0.2.tgz", + "integrity": "sha512-Aoud8H8lmlNkbRufyvRLefmGFELdBf1n5Te/Xm+Zx8ORINH+aXL+gKb5mbftFSha860+I7pMArz77TBYz8HDVg==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-constants": { "version": "57.0.12", "resolved": "https://registry.npmmirror.com/expo-constants/-/expo-constants-57.0.12.tgz", @@ -5024,6 +5309,16 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmmirror.com/makeerror/-/makeerror-1.0.12.tgz", @@ -5955,6 +6250,19 @@ "ws": "^7" } }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz", @@ -6054,6 +6362,48 @@ "react-native-reanimated": ">= 2.0.0" } }, + "node_modules/react-native-gesture-handler": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/react-native-gesture-handler/-/react-native-gesture-handler-3.2.1.tgz", + "integrity": "sha512-VEWscxUN29aeccbD2McF0LSXrwOhSdisaSRFiEWg0O/3WrVghNJp/tOtLrCgzb9gfcZ3xdA+K2Sp8F5G3iHx/Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/react-test-renderer": "^19.1.0", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-is-edge-to-edge": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz", + "integrity": "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-reanimated": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/react-native-reanimated/-/react-native-reanimated-4.6.0.tgz", + "integrity": "sha512-9vbQmok5BPX2J3RLPFt2UnwmK2QQoMDPDi0VB4a9JWIiEc55pdzvwEsdH91V/JXKNErqM6a9fElTBMc/1cbtBA==", + "license": "MIT", + "peer": true, + "dependencies": { + "react-native-is-edge-to-edge": "^1.3.1", + "semver": "^7.7.3" + }, + "peerDependencies": { + "react": "*", + "react-native": "0.83 - 0.87", + "react-native-worklets": "0.12.x" + } + }, "node_modules/react-native-safe-area-context": { "version": "5.7.0", "resolved": "https://registry.npmmirror.com/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz", @@ -6093,6 +6443,35 @@ "react-native": "*" } }, + "node_modules/react-native-worklets": { + "version": "0.12.1", + "resolved": "https://registry.npmmirror.com/react-native-worklets/-/react-native-worklets-0.12.1.tgz", + "integrity": "sha512-/u7B+Akrox32IQUi6+0OHwWAfBC/A5LVdt000RL8OKB4L1pHTvIDwVyMP6Nw7ybuuoQYS8PLlpmxTE1Ce7TqWg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/generator": "^7.27.1", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/preset-typescript": "^7.28.5", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", + "convert-source-map": "^2.0.0", + "semver": "^7.7.4" + }, + "peerDependencies": { + "@babel/core": "*", + "@react-native/metro-config": "*", + "react": "*", + "react-native": "0.83 - 0.87" + } + }, "node_modules/react-native/node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmmirror.com/commander/-/commander-12.1.0.tgz", @@ -6893,7 +7272,7 @@ "version": "6.0.3", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 0d18e13..12b5e61 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "dependencies": { "@expo/vector-icons": "^15.0.2", "expo": "~57.0.14", + "expo-blur": "~57.0.2", "expo-constants": "~57.0.12", "expo-font": "~57.0.1", "expo-linear-gradient": "~57.0.1", diff --git a/src/api/api.ts b/src/api/api.ts index c7fee24..64dbe5e 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -16,20 +16,24 @@ import type { Category, Conversation, DimensionScore, + EmployeeReview, EnterpriseEntry, HollandAssessment, HomeAITool, HotJob, Interview, Job, + JobDetail, JobLevel, JobListParams, + JobOverallScore, JobPreference, MatchData, MatchResult, MenuItem, Notification, OverallScore, + SalaryData, UserInfo, UserProfile, UserStats, @@ -500,12 +504,82 @@ const MOCK_JOB_PREFERENCE: JobPreference = { status: '积极求职中', }; -/** 勋章列表 */ +/** 勋章列表(已获得 + 未获得;未获得带获取进度) */ const MOCK_BADGES: Badge[] = [ - { id: 'badge-1', title: '求职达人', description: '投递满100次', icon: '🏅' }, - { id: 'badge-2', title: '面试高手', description: '面试10次成功', icon: '🏅' }, - { id: 'badge-3', title: 'offer收割机', description: '获得5个offer', icon: '🏅' }, - { id: 'badge-4', title: '诚信之星', description: '诚信记录满分', icon: '🏅' }, + { id: 'badge-1', title: '求职达人', description: '投递满100次', icon: '🏅', obtained: true }, + { id: 'badge-2', title: '面试高手', description: '面试10次成功', icon: '🎯', obtained: true }, + { id: 'badge-3', title: '诚信之星', description: '诚信记录满分', icon: '⭐', obtained: true }, + { id: 'badge-4', title: 'offer收割机', description: '获得5个offer', icon: '🏆', obtained: false, progress: 3, target: 5 }, + { id: 'badge-5', title: '投递达人', description: '投递满200次', icon: '📮', obtained: false, progress: 128, target: 200 }, + { id: 'badge-6', title: '学习之星', description: '完成10次测评', icon: '📚', obtained: false, progress: 4, target: 10 }, +]; + +/** 岗位详情附加/覆盖信息(id → 详情页专属字段;覆盖列表页未细化的字段) */ +const MOCK_JOB_DETAILS: Record> = { + '1': { + companyLocation: '北京 · 朝阳区', + industry: '互联网', + benefits: ['五险一金', '年终奖金', '弹性工作', '免费三餐'], + companySize: '10000人以上', + status: '刚发布', + views: 1286, + salaryMonths: 14, + }, +}; + +/** 岗位综合评分(6 维度,供雷达图) */ +const MOCK_JOB_OVERALL_SCORE: JobOverallScore = { + totalScore: 92, + level: '优秀', + rank: '超过91%同行业岗位', + dimensions: [ + { id: 'auth', name: '岗位真实性', value: 95 }, + { id: 'salary', name: '薪资竞争力', value: 88 }, + { id: 'growth', name: '发展前景', value: 92 }, + { id: 'company', name: '公司实力', value: 90 }, + { id: 'env', name: '工作环境', value: 85 }, + { id: 'benefit', name: '福利待遇', value: 82 }, + ], +}; + +/** 真实薪资区间 */ +const MOCK_SALARY_DATA: SalaryData = { + ranges: [ + { experience: '1-3年', min: 15, max: 25 }, + { experience: '3-5年', min: 25, max: 40 }, + { experience: '5-10年', min: 40, max: 65 }, + { experience: '10年以上', min: 65, max: 100 }, + ], + dataSource: '基于2,846份真实简历数据', + totalCount: 2846, +}; + +/** 员工评价列表 */ +const MOCK_EMPLOYEE_REVIEWS: EmployeeReview[] = [ + { + id: 'r-1', + userName: '张**', + userRole: '在职员工,前端开发', + time: '3天前', + content: '团队氛围很好,技术栈比较新,成长空间大,就是加班有点多...', + tags: ['团队氛围好', '技术先进'], + }, + { + id: 'r-2', + userName: '李**', + userRole: '离职员工,高级前端', + time: '2周前', + content: '薪资待遇不错,福利也还行,就是晋升通道不够清晰...', + tags: ['薪资高', '福利好'], + }, + { + id: 'r-3', + userName: '王**', + userRole: '在职员工,技术主管', + time: '1个月前', + content: '公司文化很好,同事都很优秀,能学到很多东西...', + tags: ['文化好', '成长快'], + }, ]; /* ===================== 业务规则 ===================== */ @@ -874,3 +948,70 @@ export async function fetchBadges(): Promise { // TODO: const res = await fetch(`${API_BASE_URL}/api/user/badges`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_BADGES); } + +/** + * 获取岗位详情(含详情页专属字段;等级由分数在接口层计算) + * @method GET + * @url /api/jobs/:id + * @param id { string } 岗位 id + * @returns Promise 岗位详情 + */ +export async function fetchJobDetail(id: string): Promise { + // TODO: 真实请求(后端就绪后替换) + // const res = await fetch(`${API_BASE_URL}/api/jobs/${id}`, { method: 'GET' }); + // return res.json(); + + const base = MOCK_JOBS.find((job) => job.id === id); + if (!base) throw new Error(`岗位不存在: ${id}`); + + // 详情 = 列表基础数据 + 详情页专属字段(无对应覆盖时用兜底值) + const detail: JobDetail = { + ...base, + level: scoreToLevel(base.score), + companySize: '1000-9999人', + status: '热招中', + views: 0, + salaryMonths: 13, + ...MOCK_JOB_DETAILS[id], + }; + return mockRequest(detail); +} + +/** + * 获取岗位综合评分(含 6 维度雷达图数据) + * @method GET + * @url /api/jobs/:id/score + * @param id { string } 岗位 id + * @returns Promise 综合评分 + */ +export async function fetchJobOverallScore(id: string): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/jobs/${id}/score`, { method: 'GET' }); return res.json(); + void id; + return mockRequest(MOCK_JOB_OVERALL_SCORE); +} + +/** + * 获取岗位真实薪资区间 + * @method GET + * @url /api/jobs/:id/salary + * @param id { string } 岗位 id + * @returns Promise 薪资数据 + */ +export async function fetchSalaryData(id: string): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/jobs/${id}/salary`, { method: 'GET' }); return res.json(); + void id; + return mockRequest(MOCK_SALARY_DATA); +} + +/** + * 获取员工评价列表 + * @method GET + * @url /api/jobs/:id/reviews + * @param id { string } 岗位 id + * @returns Promise 评价数组 + */ +export async function fetchEmployeeReviews(id: string): Promise { + // TODO: const res = await fetch(`${API_BASE_URL}/api/jobs/${id}/reviews`, { method: 'GET' }); return res.json(); + void id; + return mockRequest(MOCK_EMPLOYEE_REVIEWS); +} diff --git a/src/api/index.ts b/src/api/index.ts index 45ce6db..7874e4f 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -10,18 +10,22 @@ export { fetchCategories, fetchConversations, fetchDimensionScores, + fetchEmployeeReviews, fetchEnterpriseEntry, fetchHollandAssessment, fetchHomeAITools, fetchHotJobs, fetchInterviews, + fetchJobDetail, fetchJobList, + fetchJobOverallScore, fetchJobPreference, fetchMatchData, fetchMatchResult, fetchMenuItems, fetchNotifications, fetchOverallScore, + fetchSalaryData, fetchUserInfo, fetchUserProfile, fetchUserStats, diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 3d02979..f8de3f0 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -5,6 +5,7 @@ export { useAsyncData } from './useAsyncData'; export { useCategoryData } from './useCategoryData'; export { useGrowthData } from './useGrowthData'; export { useHomeData } from './useHomeData'; +export { useJobDetail } from './useJobDetail'; export { useJobList } from './useJobList'; export { useMatchData } from './useMatchData'; export { useMessageData } from './useMessageData'; diff --git a/src/hooks/useJobDetail.ts b/src/hooks/useJobDetail.ts new file mode 100644 index 0000000..b73ef71 --- /dev/null +++ b/src/hooks/useJobDetail.ts @@ -0,0 +1,50 @@ +/** + * 岗位详情数据 hook:并行组合岗位详情、综合评分、薪资区间、员工评价四类数据, + * 统一管理 loading / error / refresh 三态。 + */ +import { useCallback } from 'react'; +import { + fetchEmployeeReviews, + fetchJobDetail, + fetchJobOverallScore, + fetchSalaryData, +} from '../api'; +import type { + EmployeeReview, + JobDetail, + JobOverallScore, + SalaryData, +} from '../types'; +import { useAsyncData } from './useAsyncData'; + +/** 岗位详情聚合数据 */ +interface JobDetailData { + detail: JobDetail; + overallScore: JobOverallScore; + salaryData: SalaryData; + reviews: EmployeeReview[]; +} + +export function useJobDetail(id: string) { + const fetcher = useCallback(async (): Promise => { + const [detail, overallScore, salaryData, reviews] = await Promise.all([ + fetchJobDetail(id), + fetchJobOverallScore(id), + fetchSalaryData(id), + fetchEmployeeReviews(id), + ]); + return { detail, overallScore, salaryData, reviews }; + }, [id]); + + const { data, loading, error, refresh } = useAsyncData(fetcher); + + return { + detail: data?.detail ?? null, + overallScore: data?.overallScore ?? null, + salaryData: data?.salaryData ?? null, + reviews: data?.reviews ?? [], + loading, + error, + refresh, + }; +} diff --git a/src/types/index.ts b/src/types/index.ts index 8a39e3c..e483632 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -3,10 +3,16 @@ */ export type { Category, + EmployeeReview, Job, + JobDetail, + JobDimensionScore, JobLevel, JobListParams, + JobOverallScore, MatchData, + SalaryData, + SalaryRange, SortType, } from './job'; export type { diff --git a/src/types/job.ts b/src/types/job.ts index 316dd8f..f66ba3e 100644 --- a/src/types/job.ts +++ b/src/types/job.ts @@ -77,3 +77,71 @@ export interface JobListParams { pageSize?: number; } +/** 岗位详情(继承 Job,增加详情页专属字段;industry/benefits 复用 Job 已有定义) */ +export interface JobDetail extends Job { + /** 公司规模,如 "10000人以上" */ + companySize: string; + /** 发布状态,如 "刚发布" */ + status: string; + /** 浏览次数,如 1286 */ + views: number; + /** 薪资月数,如 14(展示为 "14薪") */ + salaryMonths: number; +} + +/** 岗位详情维度评分(雷达图 6 维度,与用户端 DimensionScore 区分命名) */ +export interface JobDimensionScore { + id: string; + /** 维度名称,如 "岗位真实性" */ + name: string; + /** 分值(0-100) */ + value: number; +} + +/** 岗位综合评分 */ +export interface JobOverallScore { + /** 总分,如 92 */ + totalScore: number; + /** 等级,如 "优秀" */ + level: string; + /** 排名文案,如 "超过91%同行业岗位" */ + rank: string; + /** 6 个维度评分 */ + dimensions: JobDimensionScore[]; +} + +/** 薪资区间(按经验分段) */ +export interface SalaryRange { + /** 经验段,如 "1-3年" */ + experience: string; + /** 最低薪资(K) */ + min: number; + /** 最高薪资(K) */ + max: number; +} + +/** 薪资数据(真实薪资区间) */ +export interface SalaryData { + /** 各经验段薪资区间 */ + ranges: SalaryRange[]; + /** 数据来源说明,如 "基于2,846份真实简历数据" */ + dataSource: string; + /** 样本总数,如 2846 */ + totalCount: number; +} + +/** 员工评价 */ +export interface EmployeeReview { + id: string; + /** 用户名(脱敏),如 "张**" */ + userName: string; + /** 身份标签,如 "在职员工,前端开发" */ + userRole: string; + /** 时间,如 "3天前" */ + time: string; + /** 评价内容 */ + content: string; + /** 评价标签 */ + tags: string[]; +} + diff --git a/src/types/user.ts b/src/types/user.ts index cc983c3..d7cbd74 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -98,4 +98,10 @@ export interface Badge { description: string; /** 勋章图标(emoji) */ icon: string; + /** 是否已获得 */ + obtained: boolean; + /** 获取进度(未获得时展示进度条,如 3) */ + progress?: number; + /** 进度目标(未获得时的分母,如 5) */ + target?: number; }