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 (
);
}
/* ===================== 子组件:顶部导航 ===================== */
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 (
);
}
if (error || !result) {
return (
);
}
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 },
});