import { Ionicons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { useRouter } from 'expo-router'; import { ActivityIndicator, Alert, Pressable, ScrollView, StyleSheet, Text, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import Svg, { Circle, Line, Polygon } from 'react-native-svg'; import { useHollandAssessment } from '../src/hooks'; import { buildHollandReportText } from '../src/api'; import { shareText } from '../src/lib/shareFile'; import { colors, headerGradientShort, hollandTypeColors } from '../src/theme'; import type { HollandAction, HollandDimension, HollandTypeAnalysis } from '../src/types'; const RADAR_SIZE = 160; const RADAR_CENTER = RADAR_SIZE / 2; const RADAR_MAX_RADIUS = RADAR_SIZE / 2 - 8; function radarPoint(index: number, total: number, radius: number) { const angle = (Math.PI * 2 * index) / total - Math.PI / 2; return { x: RADAR_CENTER + radius * Math.cos(angle), y: RADAR_CENTER + radius * Math.sin(angle), }; } function RadarChart({ dimensions }: { dimensions: HollandDimension[] }) { const total = dimensions.length; const gridPolygons = [1, 2, 3].map((level) => dimensions .map((_, i) => { const p = radarPoint(i, total, (RADAR_MAX_RADIUS * level) / 3); return `${p.x},${p.y}`; }) .join(' '), ); const axes = dimensions.map((_, i) => radarPoint(i, total, RADAR_MAX_RADIUS)); const dataVertices = dimensions.map((d, i) => radarPoint(i, total, (RADAR_MAX_RADIUS * d.percent) / 100), ); const dataPoints = dataVertices.map((p) => `${p.x},${p.y}`).join(' '); return ( {gridPolygons.map((points, i) => ( ))} {axes.map((p, i) => ( ))} {dataVertices.map((p, i) => ( ))} ); } /* ===================== 子组件:顶部导航 ===================== */ function Header({ onBack, onShare }: { onBack: () => void; onShare?: () => void }) { return ( 测评结果 {onShare ? ( ) : ( )} ); } /* ===================== 子组件:结果 Hero ===================== */ function ResultHero({ code, name, dimensions, }: { code: string; name: string; dimensions: HollandDimension[]; }) { return ( 你的职业兴趣类型 {code} 型 {name} ); } /* ===================== 子组件:主导类型解析 ===================== */ function TypeCard({ analysis, index }: { analysis: HollandTypeAnalysis; index: number }) { const rankColors = [colors.primary, colors.success, colors.info]; return ( {index + 1} {analysis.name}({analysis.type}) {analysis.score}分 {analysis.description} {analysis.careers.map((career) => ( {career} ))} ); } /* ===================== 子组件:推荐岗位 ===================== */ function JobCard({ job }: { job: import('../src/types').HollandJobRecommendation }) { return ( {job.title} {job.companyName} · {job.city} 匹配度 {job.match}% {job.tags.map((tag) => ( {tag} ))} {job.salary} ); } /* ===================== 子组件:行动建议 ===================== */ function ActionCard({ action }: { action: HollandAction }) { const router = useRouter(); // 建议卡的目标路由由接口层给出(见 api.ts 的 actions);没有 route 的只做展示,不带箭头 const route = action.route; const body = ( <> {action.title} {action.desc} {route ? : null} ); if (!route) { return {body}; } return ( router.push(route as never)}> {body} ); } /* ===================== 子组件:四态 ===================== */ function Loading() { return ( 正在加载结果… ); } function ErrorState({ onRetry }: { onRetry: () => void }) { return ( 结果加载失败 点击重试 ); } /* ===================== 页面 ===================== */ export default function HollandResultScreen() { const router = useRouter(); const { assessment, loading, error, refresh } = useHollandAssessment(); const result = assessment?.result; /** * 分享测评报告:把结果拼成文本经系统分享面板发出。 * `ok` 只代表面板已拉起,不能据此提示「分享成功」,只在失败时说明原因(见 lib/shareFile)。 */ const handleShare = async () => { if (!result) return; const outcome = await shareText(buildHollandReportText(result), '霍兰德职业兴趣测评报告'); if (!outcome.ok) Alert.alert('无法分享', outcome.message); }; if (loading) { return (
router.back()} /> ); } if (error || !result) { return (
router.back()} /> ); } return (
router.back()} onShare={handleShare} /> 主导类型解析 {result.analyses.map((a, i) => ( ))} 为你推荐的岗位 {result.jobs.map((job) => ( ))} 下一步行动建议 {result.actions.map((action) => ( ))} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.pageBg }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 12, paddingVertical: 12, backgroundColor: colors.white, }, headerBtn: { width: 40, alignItems: 'center' }, headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary }, body: { padding: 8, paddingBottom: 32 }, heroCard: { borderRadius: 8, padding: 8, flexDirection: 'row', alignItems: 'center', marginBottom: 8, }, heroLeft: { flex: 1 }, heroLabel: { fontSize: 13, color: colors.textInverse, opacity: 0.9 }, heroCode: { fontSize: 42, fontWeight: '800', color: colors.textInverse, marginTop: 6 }, heroName: { fontSize: 14, color: colors.textInverse, opacity: 0.95, marginTop: 6, lineHeight: 20 }, heroRadar: { marginLeft: 12 }, section: { marginBottom: 16 }, sectionTitle: { fontSize: 18, fontWeight: '700', color: colors.textPrimary, marginBottom: 12 }, typeCard: { backgroundColor: colors.white, borderRadius: 8, padding: 8, marginBottom: 8, borderWidth: 1, borderColor: colors.borderLight, }, typeHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 10 }, typeRank: { width: 24, height: 24, borderRadius: 12, alignItems: 'center', justifyContent: 'center', }, typeRankText: { fontSize: 13, fontWeight: '800', color: colors.white }, typeTitleWrap: { flex: 1, marginLeft: 10 }, typeName: { fontSize: 15, fontWeight: '700', color: colors.textPrimary }, typeScore: { fontSize: 13, color: colors.primary, marginTop: 2 }, typeDesc: { fontSize: 13, color: colors.textSecondary, lineHeight: 20 }, careers: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 10 }, careerTag: { backgroundColor: colors.primaryLight, borderRadius: 10, paddingHorizontal: 10, paddingVertical: 4, }, careerTagText: { fontSize: 12, color: colors.primaryText }, jobCard: { backgroundColor: colors.white, borderRadius: 8, padding: 8, marginBottom: 8, borderWidth: 1, borderColor: colors.borderLight, }, jobTop: { flexDirection: 'row', justifyContent: 'space-between' }, jobTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary }, jobCompany: { fontSize: 12, color: colors.textSecondary, marginTop: 2 }, matchTag: { backgroundColor: colors.successLight, borderRadius: 10, paddingHorizontal: 8, paddingVertical: 2, }, matchText: { fontSize: 11, color: colors.successText, fontWeight: '600' }, jobTags: { flexDirection: 'row', gap: 8, marginTop: 10 }, tag: { backgroundColor: colors.pageBg, borderRadius: 8, paddingHorizontal: 8, paddingVertical: 2, }, tagText: { fontSize: 11, color: colors.textSecondary }, jobSalary: { fontSize: 16, fontWeight: '800', color: colors.danger, marginTop: 10 }, actionCard: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.white, borderRadius: 8, padding: 8, marginBottom: 8, borderWidth: 1, borderColor: colors.borderLight, }, actionBody: { flex: 1, marginLeft: 12 }, actionTitle: { fontSize: 14, fontWeight: '700', color: colors.textPrimary }, actionDesc: { fontSize: 12, color: colors.textSecondary, marginTop: 2 }, center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 40, gap: 12 }, tip: { fontSize: 14, color: colors.textSecondary }, retryBtn: { paddingHorizontal: 18, paddingVertical: 8, borderRadius: 16, backgroundColor: colors.primaryLight }, retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText }, });