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.
380 lines
12 KiB
380 lines
12 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 Svg, { Circle, Line, Polygon } from 'react-native-svg';
|
|
import { useHollandAssessment } from '../src/hooks';
|
|
import { colors, headerGradientShort } from '../src/theme';
|
|
import type { HollandDimension, HollandTypeAnalysis } from '../src/types';
|
|
|
|
const RADAR_SIZE = 160;
|
|
const RADAR_CENTER = RADAR_SIZE / 2;
|
|
const RADAR_MAX_RADIUS = RADAR_SIZE / 2 - 8;
|
|
|
|
const TYPE_COLORS: Record<string, string> = {
|
|
R: colors.primary,
|
|
I: colors.info,
|
|
A: colors.action,
|
|
S: colors.success,
|
|
E: '#FFB347',
|
|
C: colors.textHint,
|
|
};
|
|
|
|
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 (
|
|
<Svg width={RADAR_SIZE} height={RADAR_SIZE}>
|
|
{gridPolygons.map((points, i) => (
|
|
<Polygon key={`grid-${i}`} points={points} fill="none" stroke={colors.borderStrong} strokeWidth={1} />
|
|
))}
|
|
{axes.map((p, i) => (
|
|
<Line
|
|
key={`axis-${i}`}
|
|
x1={RADAR_CENTER}
|
|
y1={RADAR_CENTER}
|
|
x2={p.x}
|
|
y2={p.y}
|
|
stroke={colors.borderStrong}
|
|
strokeWidth={1}
|
|
/>
|
|
))}
|
|
<Polygon
|
|
points={dataPoints}
|
|
fill={colors.primary}
|
|
fillOpacity={0.22}
|
|
stroke={colors.primary}
|
|
strokeWidth={2}
|
|
/>
|
|
{dataVertices.map((p, i) => (
|
|
<Circle key={`dot-${i}`} cx={p.x} cy={p.y} r={3} fill={TYPE_COLORS[dimensions[i].type]} />
|
|
))}
|
|
</Svg>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:顶部导航 ===================== */
|
|
|
|
function Header({ onBack, onShare }: { onBack: () => void; onShare: () => 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}>测评结果</Text>
|
|
<Pressable style={styles.headerBtn} onPress={onShare} hitSlop={8}>
|
|
<Ionicons name="share-outline" size={22} color={colors.textPrimary} />
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:结果 Hero ===================== */
|
|
|
|
function ResultHero({
|
|
code,
|
|
name,
|
|
dimensions,
|
|
}: {
|
|
code: string;
|
|
name: string;
|
|
dimensions: HollandDimension[];
|
|
}) {
|
|
return (
|
|
<LinearGradient
|
|
colors={headerGradientShort}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
style={styles.heroCard}
|
|
>
|
|
<View style={styles.heroLeft}>
|
|
<Text style={styles.heroLabel}>你的职业兴趣类型</Text>
|
|
<Text style={styles.heroCode}>{code} 型</Text>
|
|
<Text style={styles.heroName}>{name}</Text>
|
|
</View>
|
|
<View style={styles.heroRadar}>
|
|
<RadarChart dimensions={dimensions} />
|
|
</View>
|
|
</LinearGradient>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:主导类型解析 ===================== */
|
|
|
|
function TypeCard({ analysis, index }: { analysis: HollandTypeAnalysis; index: number }) {
|
|
const rankColors = [colors.primary, colors.success, colors.info];
|
|
return (
|
|
<View style={styles.typeCard}>
|
|
<View style={styles.typeHeader}>
|
|
<View style={[styles.typeRank, { backgroundColor: rankColors[index] ?? colors.primary }]}>
|
|
<Text style={styles.typeRankText}>{index + 1}</Text>
|
|
</View>
|
|
<View style={styles.typeTitleWrap}>
|
|
<Text style={styles.typeName}>
|
|
{analysis.name}({analysis.type})
|
|
</Text>
|
|
<Text style={styles.typeScore}>{analysis.score}分</Text>
|
|
</View>
|
|
</View>
|
|
<Text style={styles.typeDesc}>{analysis.description}</Text>
|
|
<View style={styles.careers}>
|
|
{analysis.careers.map((career) => (
|
|
<View key={career} style={styles.careerTag}>
|
|
<Text style={styles.careerTagText}>{career}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:推荐岗位 ===================== */
|
|
|
|
function JobCard({ job }: { job: import('../src/types').HollandJobRecommendation }) {
|
|
return (
|
|
<View style={styles.jobCard}>
|
|
<View style={styles.jobTop}>
|
|
<View>
|
|
<Text style={styles.jobTitle}>{job.title}</Text>
|
|
<Text style={styles.jobCompany}>{job.companyName} · {job.city}</Text>
|
|
</View>
|
|
<View style={styles.matchTag}>
|
|
<Text style={styles.matchText}>匹配度 {job.match}%</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.jobTags}>
|
|
{job.tags.map((tag) => (
|
|
<View key={tag} style={styles.tag}>
|
|
<Text style={styles.tagText}>{tag}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
<Text style={styles.jobSalary}>{job.salary}</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:行动建议 ===================== */
|
|
|
|
function ActionCard({ action }: { action: import('../src/types').HollandAction }) {
|
|
return (
|
|
<Pressable style={styles.actionCard}>
|
|
<Ionicons name={action.icon as keyof typeof Ionicons.glyphMap} size={22} color={colors.primary} />
|
|
<View style={styles.actionBody}>
|
|
<Text style={styles.actionTitle}>{action.title}</Text>
|
|
<Text style={styles.actionDesc}>{action.desc}</Text>
|
|
</View>
|
|
<Ionicons name="chevron-forward" size={18} color={colors.textHint} />
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:四态 ===================== */
|
|
|
|
function Loading() {
|
|
return (
|
|
<View style={styles.center}>
|
|
<ActivityIndicator size="large" color={colors.primary} />
|
|
<Text style={styles.tip}>正在加载结果…</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function ErrorState({ onRetry }: { onRetry: () => void }) {
|
|
return (
|
|
<View style={styles.center}>
|
|
<Ionicons name="help-circle-outline" size={40} color={colors.textHint} />
|
|
<Text style={styles.tip}>结果加载失败</Text>
|
|
<Pressable style={styles.retryBtn} onPress={onRetry} hitSlop={6}>
|
|
<Text style={styles.retryText}>点击重试</Text>
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 页面 ===================== */
|
|
|
|
export default function HollandResultScreen() {
|
|
const router = useRouter();
|
|
const { assessment, loading, error, refresh } = useHollandAssessment();
|
|
const result = assessment?.result;
|
|
|
|
if (loading) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<Header onBack={() => router.back()} onShare={() => {}} />
|
|
<Loading />
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
if (error || !result) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<Header onBack={() => router.back()} onShare={() => {}} />
|
|
<ErrorState onRetry={refresh} />
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<Header onBack={() => router.back()} onShare={() => {}} />
|
|
<ScrollView contentContainerStyle={styles.body} showsVerticalScrollIndicator={false}>
|
|
<ResultHero code={result.code} name={result.name} dimensions={result.dimensions} />
|
|
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>主导类型解析</Text>
|
|
{result.analyses.map((a, i) => (
|
|
<TypeCard key={a.type} analysis={a} index={i} />
|
|
))}
|
|
</View>
|
|
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>为你推荐的岗位</Text>
|
|
{result.jobs.map((job) => (
|
|
<JobCard key={job.id} job={job} />
|
|
))}
|
|
</View>
|
|
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>下一步行动建议</Text>
|
|
{result.actions.map((action) => (
|
|
<ActionCard key={action.id} action={action} />
|
|
))}
|
|
</View>
|
|
</ScrollView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
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: 16, paddingBottom: 32 },
|
|
heroCard: {
|
|
borderRadius: 16,
|
|
padding: 20,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginBottom: 16,
|
|
},
|
|
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: 12,
|
|
padding: 16,
|
|
marginBottom: 10,
|
|
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: 12,
|
|
padding: 14,
|
|
marginBottom: 10,
|
|
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: 12,
|
|
padding: 14,
|
|
marginBottom: 10,
|
|
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 },
|
|
});
|
|
|