轻松聘
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

664 lines
17 KiB

import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router';
import {
ActivityIndicator,
Pressable,
ScrollView,
StyleSheet,
Text,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useOfferCompareData } from '../../src/hooks';
import { colors, headerGradientSolid } from '../../src/theme';
import type { OfferBenefitGroup, OfferItem, OfferScore, WorkLifeBalanceRow } from '../../src/types';
/* ===================== 子组件 ===================== */
/** 顶部导航:返回 + 标题 + 编辑 */
function Header({ onBack, onEdit }: { onBack: () => void; onEdit?: () => void }) {
return (
<View style={styles.header}>
<Pressable style={styles.headerBtn} onPress={onBack} hitSlop={8}>
<Ionicons name="chevron-back" size={24} color={colors.textPrimary} />
</Pressable>
<Text style={styles.headerTitle}>Offer对比</Text>
<Pressable style={styles.headerBtn} onPress={onEdit} hitSlop={8}>
<Text style={styles.headerEdit}>编辑</Text>
</Pressable>
</View>
);
}
/** 单个 Offer 卡片 */
function OfferCard({ offer, selected }: { offer: OfferItem; selected: boolean }) {
return (
<View style={[styles.offerCard, selected && styles.offerCardSelected]}>
<View style={styles.offerCardTop}>
<Text style={styles.offerCompany} numberOfLines={1}>
{offer.company}
</Text>
<View style={[styles.checkCircle, selected && styles.checkCircleActive]}>
{selected && <Ionicons name="checkmark" size={12} color={colors.white} />}
</View>
</View>
<Text style={styles.offerPosition} numberOfLines={1}>
{offer.position}
</Text>
<Text style={[styles.offerSalary, { color: offer.color }]}>{offer.salary}</Text>
</View>
);
}
/** 添加 Offer */
function AddOfferCard({ onPress }: { onPress: () => void }) {
return (
<Pressable style={styles.addOfferCard} onPress={onPress}>
<Ionicons name="add" size={28} color={colors.textHint} />
<Text style={styles.addOfferText}>添加Offer</Text>
</Pressable>
);
}
/** 星级评分 */
function StarRating({ stars }: { stars: number }) {
return (
<View style={styles.starRow}>
{[1, 2, 3, 4, 5].map((i) => (
<Ionicons
key={i}
name={i <= stars ? 'star' : 'star-outline'}
size={12}
color={i <= stars ? colors.action : colors.borderStrong}
style={styles.starIcon}
/>
))}
</View>
);
}
/** 综合评分对比 */
function ScoreComparison({ offers, scores }: { offers: OfferItem[]; scores: OfferScore[] }) {
return (
<View style={styles.card}>
<View style={styles.sectionHeader}>
<View style={styles.sectionBar} />
<Text style={styles.sectionTitle}>综合评分对比</Text>
</View>
<View style={styles.scoreRow}>
{offers.map((offer, index) => {
const score = scores[index];
if (!score) return null;
const isWinner = score.score === Math.max(...scores.map((s) => s.score));
return (
<View key={offer.id} style={styles.scoreCol}>
<Text style={[styles.scoreNum, isWinner && styles.scoreNumWinner]}>{score.score}</Text>
<View style={styles.scoreMeta}>
<Text style={[styles.scoreLevel, { color: offer.color }]}>{score.level}</Text>
<StarRating stars={score.stars} />
</View>
<Text style={styles.scoreHint}>推荐指数</Text>
</View>
);
})}
</View>
</View>
);
}
/** 分组对比表格 */
function BenefitComparison({ offers, groups }: { offers: OfferItem[]; groups: OfferBenefitGroup[] }) {
return (
<View style={styles.card}>
<View style={styles.sectionHeader}>
<View style={styles.sectionBar} />
<Text style={styles.sectionTitle}>薪资福利对比</Text>
</View>
{/* 表头:公司名 */}
<View style={styles.tableHeaderRow}>
<View style={styles.tableLabelCell} />
{offers.map((offer) => (
<View key={offer.id} style={styles.tableValueCell}>
<View style={[styles.offerDot, { backgroundColor: offer.color }]} />
<Text style={styles.tableHeaderText} numberOfLines={1}>
{offer.company}
</Text>
</View>
))}
</View>
{groups.map((group) => (
<View key={group.title}>
<Text style={styles.groupTitle}>{group.title}</Text>
{group.rows.map((row) => (
<View key={row.label} style={styles.tableRow}>
<Text style={styles.tableLabelText}>{row.label}</Text>
{row.values.map((value, colIndex) => {
const highlight = row.highlights?.[colIndex];
return (
<View
key={colIndex}
style={[
styles.tableValueCell,
highlight && { backgroundColor: colors.primaryLight },
]}
>
<Text style={[styles.tableValueText, highlight && styles.tableValueTextHighlight]}>
{value ?? '-'}
</Text>
</View>
);
})}
</View>
))}
</View>
))}
</View>
);
}
/** 工作生活平衡 */
function WorkLifeBalance({ offers, rows }: { offers: OfferItem[]; rows: WorkLifeBalanceRow[] }) {
return (
<View style={styles.card}>
<View style={styles.sectionHeader}>
<View style={styles.sectionBar} />
<Text style={styles.sectionTitle}>工作生活平衡</Text>
</View>
<View style={styles.tableHeaderRow}>
<View style={styles.tableLabelCell} />
{offers.map((offer) => (
<View key={offer.id} style={styles.tableValueCell}>
<Text style={styles.tableHeaderText} numberOfLines={1}>
{offer.company}
</Text>
</View>
))}
</View>
{rows.map((row) => (
<View key={row.label} style={styles.tableRow}>
<Text style={styles.tableLabelText}>{row.label}</Text>
{row.values.map((value, colIndex) => {
const highlight = row.highlights?.[colIndex];
return (
<View
key={colIndex}
style={[styles.tableValueCell, highlight && { backgroundColor: colors.successLight }]}
>
<Text style={[styles.tableValueText, highlight && styles.tableValueTextSuccess]}>
{value}
</Text>
</View>
);
})}
</View>
))}
</View>
);
}
/** AI 智能建议 */
function AISuggestionCard({ summary, detail, reportAction }: { summary: string; detail: string; reportAction: string }) {
return (
<LinearGradient colors={headerGradientSolid} start={{ x: 0, y: 0 }} end={{ x: 1, y: 1 }} style={styles.aiCard}>
<View style={styles.aiHeader}>
<Ionicons name="sparkles" size={18} color={colors.white} />
<Text style={styles.aiTitle}>AI智能建议</Text>
</View>
<Text style={styles.aiSummary}>{summary}</Text>
<Text style={styles.aiDetail}>{detail}</Text>
<Pressable style={styles.aiReportBtn} onPress={() => {}}>
<Text style={styles.aiReportText}>{reportAction}</Text>
<Ionicons name="chevron-forward" size={14} color={colors.white} />
</Pressable>
</LinearGradient>
);
}
/* ===================== 页面 ===================== */
/**
* Offer 对比页:对比已收到 Offer 的薪资、福利、工作生活平衡与 AI 建议
*/
export default function OfferCompareScreen() {
const router = useRouter();
const { data, offers, scores, benefitGroups, workLifeBalance, aiSuggestion, actions, loading, error, refresh } =
useOfferCompareData();
if (error) {
return (
<SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}>
<Header onBack={() => router.back()} />
<View style={styles.stateWrap}>
<Text style={styles.stateText}>加载失败,请重试</Text>
<Pressable style={styles.retryBtn} onPress={refresh}>
<Text style={styles.retryText}>重试</Text>
</Pressable>
</View>
</SafeAreaView>
);
}
if (loading || !data) {
return (
<SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}>
<Header onBack={() => router.back()} />
<View style={styles.stateWrap}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={styles.stateText}>正在加载 Offer 对比…</Text>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}>
<Header onBack={() => router.back()} />
<ScrollView
style={styles.scroll}
contentContainerStyle={styles.content}
showsVerticalScrollIndicator={false}
>
{/* Offer 卡片头部 */}
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.offerList}>
{offers.map((offer) => (
<OfferCard key={offer.id} offer={offer} selected={offer.selected} />
))}
<AddOfferCard onPress={() => {}} />
</ScrollView>
{/* 综合评分对比 */}
<ScoreComparison offers={offers} scores={scores} />
{/* 薪资福利对比 */}
<BenefitComparison offers={offers} groups={benefitGroups} />
{/* 工作生活平衡 */}
<WorkLifeBalance offers={offers} rows={workLifeBalance} />
{/* AI 智能建议 */}
{aiSuggestion && (
<AISuggestionCard
summary={aiSuggestion.summary}
detail={aiSuggestion.detail}
reportAction={aiSuggestion.reportAction}
/>
)}
<View style={styles.bottomGap} />
</ScrollView>
{/* 底部操作栏 */}
<View style={styles.actionBar}>
<Pressable style={styles.exportBtn} onPress={() => {}}>
<Text style={styles.exportText}>{actions?.exportLabel ?? '导出对比报告'}</Text>
</Pressable>
<Pressable style={styles.acceptBtn} onPress={() => {}}>
<Text style={styles.acceptText}>{actions?.acceptLabel ?? '接受Offer'}</Text>
</Pressable>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: colors.pageBg,
},
/* ---- 顶部导航 ---- */
header: {
height: 48,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
backgroundColor: colors.pageBg,
},
headerBtn: {
width: 48,
height: 40,
alignItems: 'center',
justifyContent: 'center',
},
headerTitle: {
flex: 1,
textAlign: 'center',
fontSize: 17,
fontWeight: '700',
color: colors.textPrimary,
},
headerEdit: {
fontSize: 14,
color: colors.primary,
fontWeight: '600',
},
/* ---- 状态 ---- */
stateWrap: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 24,
},
stateText: {
marginTop: 12,
fontSize: 14,
color: colors.textSecondary,
textAlign: 'center',
},
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
retryText: {
fontSize: 13,
fontWeight: '600',
color: colors.white,
},
/* ---- 页面内容 ---- */
scroll: {
flex: 1,
},
content: {
paddingHorizontal: 12,
paddingTop: 8,
paddingBottom: 12,
},
bottomGap: {
height: 80,
},
/* ---- Offer 卡片 ---- */
offerList: {
gap: 10,
paddingVertical: 4,
paddingHorizontal: 4,
},
offerCard: {
width: 150,
padding: 14,
borderRadius: 12,
backgroundColor: colors.white,
borderWidth: 1,
borderColor: colors.border,
},
offerCardSelected: {
borderColor: colors.primary,
backgroundColor: colors.primaryLight,
},
offerCardTop: {
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
},
offerCompany: {
flex: 1,
fontSize: 14,
fontWeight: '700',
color: colors.textPrimary,
},
checkCircle: {
width: 18,
height: 18,
borderRadius: 9,
borderWidth: 1.5,
borderColor: colors.borderStrong,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 6,
},
checkCircleActive: {
backgroundColor: colors.primary,
borderColor: colors.primary,
},
offerPosition: {
marginTop: 6,
fontSize: 12,
color: colors.textSecondary,
},
offerSalary: {
marginTop: 10,
fontSize: 16,
fontWeight: '800',
},
addOfferCard: {
width: 80,
padding: 12,
borderRadius: 12,
borderWidth: 1,
borderStyle: 'dashed',
borderColor: colors.borderStrong,
alignItems: 'center',
justifyContent: 'center',
},
addOfferText: {
marginTop: 6,
fontSize: 12,
color: colors.textHint,
},
/* ---- 通用卡片与分区标题 ---- */
card: {
backgroundColor: colors.white,
borderRadius: 12,
padding: 16,
marginTop: 12,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 14,
},
sectionBar: {
width: 4,
height: 16,
borderRadius: 2,
backgroundColor: colors.primary,
marginRight: 8,
},
sectionTitle: {
fontSize: 16,
fontWeight: '700',
color: colors.textPrimary,
},
/* ---- 综合评分对比 ---- */
scoreRow: {
flexDirection: 'row',
justifyContent: 'space-around',
paddingTop: 4,
},
scoreCol: {
alignItems: 'center',
flex: 1,
},
scoreNum: {
fontSize: 44,
fontWeight: '800',
color: colors.textSecondary,
},
scoreNumWinner: {
color: colors.success,
},
scoreMeta: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 6,
gap: 6,
},
scoreLevel: {
fontSize: 13,
fontWeight: '700',
},
starRow: {
flexDirection: 'row',
},
starIcon: {
marginHorizontal: 1,
},
scoreHint: {
marginTop: 6,
fontSize: 11,
color: colors.textHint,
},
/* ---- 表格 ---- */
tableHeaderRow: {
flexDirection: 'row',
alignItems: 'center',
paddingBottom: 10,
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
},
tableRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
},
tableLabelCell: {
width: 88,
},
tableLabelText: {
width: 88,
fontSize: 13,
color: colors.textSecondary,
},
tableValueCell: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 8,
marginHorizontal: 2,
borderRadius: 8,
},
tableHeaderText: {
fontSize: 13,
fontWeight: '600',
color: colors.textPrimary,
textAlign: 'center',
},
tableValueText: {
fontSize: 13,
color: colors.textPrimary,
textAlign: 'center',
},
tableValueTextHighlight: {
color: colors.primaryText,
fontWeight: '700',
},
tableValueTextSuccess: {
color: colors.successText,
fontWeight: '700',
},
offerDot: {
width: 6,
height: 6,
borderRadius: 3,
marginRight: 4,
},
groupTitle: {
marginTop: 14,
marginBottom: 8,
fontSize: 14,
fontWeight: '700',
color: colors.textPrimary,
},
/* ---- AI 建议 ---- */
aiCard: {
marginTop: 12,
borderRadius: 12,
padding: 16,
},
aiHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 10,
},
aiTitle: {
marginLeft: 6,
fontSize: 16,
fontWeight: '700',
color: colors.textInverse,
},
aiSummary: {
fontSize: 14,
fontWeight: '700',
color: colors.textInverse,
lineHeight: 20,
},
aiDetail: {
marginTop: 8,
fontSize: 13,
color: colors.textInverse,
opacity: 0.9,
lineHeight: 19,
},
aiReportBtn: {
marginTop: 12,
flexDirection: 'row',
alignItems: 'center',
},
aiReportText: {
fontSize: 13,
color: colors.textInverse,
fontWeight: '600',
},
/* ---- 底部操作栏 ---- */
actionBar: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingTop: 10,
paddingBottom: 16,
backgroundColor: colors.white,
borderTopWidth: 1,
borderTopColor: colors.borderLight,
gap: 10,
},
exportBtn: {
flex: 1,
height: 44,
borderRadius: 22,
borderWidth: 1,
borderColor: colors.border,
alignItems: 'center',
justifyContent: 'center',
},
exportText: {
fontSize: 14,
fontWeight: '600',
color: colors.textPrimary,
},
acceptBtn: {
flex: 1.4,
height: 44,
borderRadius: 22,
backgroundColor: colors.primaryBtn,
alignItems: 'center',
justifyContent: 'center',
},
acceptText: {
fontSize: 14,
fontWeight: '700',
color: colors.white,
},
});