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.
382 lines
14 KiB
382 lines
14 KiB
import { Ionicons } from '@expo/vector-icons';
|
|
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, Path, Text as SvgText } from 'react-native-svg';
|
|
import { useAbilityAssessment } from '../src/hooks';
|
|
import { colors } from '../src/theme';
|
|
import { JOB_LEVEL_LABEL } from '../src/constants';
|
|
import type { AbilityDimension, AbilityTrendPoint, JobLevel } from '../src/types';
|
|
|
|
const LEVEL_TEXT_COLOR: Record<JobLevel, string> = {
|
|
excellent: colors.primaryDark,
|
|
good: colors.primary,
|
|
medium: colors.textSecondary,
|
|
};
|
|
|
|
const DIM_COLORS = [colors.primary, colors.info, colors.success, colors.action];
|
|
const DIM_LABELS = ['专业能力', '项目经验', '技能广度', '成长潜力'];
|
|
|
|
/* ===================== 子组件:顶部导航 ===================== */
|
|
|
|
function Header({ onBack, onRefresh }: { onBack: () => void; onRefresh: () => 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={onRefresh} hitSlop={8}>
|
|
<Ionicons name="refresh-outline" size={22} color={colors.textPrimary} />
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:综合评分卡 ===================== */
|
|
|
|
function ScoreCard({
|
|
score,
|
|
level,
|
|
percentile,
|
|
}: {
|
|
score: number;
|
|
level: JobLevel;
|
|
percentile?: number;
|
|
}) {
|
|
return (
|
|
<View style={styles.scoreCard}>
|
|
<View style={styles.scoreLeft}>
|
|
<Text style={styles.scoreLabel}>四大核心能力综合测评</Text>
|
|
<View style={styles.scoreRow}>
|
|
<Text style={styles.scoreValue}>{score}</Text>
|
|
<Text style={styles.scoreUnit}>分</Text>
|
|
<View style={[styles.levelTag, { backgroundColor: colors.primaryLight }]}>
|
|
<Text style={[styles.levelText, { color: LEVEL_TEXT_COLOR[level] }]}>
|
|
{JOB_LEVEL_LABEL[level]}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
<Text style={styles.percentile}>超过 {percentile ?? 0}% 同岗位求职者</Text>
|
|
</View>
|
|
<View style={styles.scoreIcon}>
|
|
<Ionicons name="cube-outline" size={40} color={colors.primary} />
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:能力维度卡片 ===================== */
|
|
|
|
function DimensionCard({ dim, index }: { dim: AbilityDimension; index: number }) {
|
|
const color = DIM_COLORS[index % DIM_COLORS.length];
|
|
return (
|
|
<View style={styles.dimCard}>
|
|
<View style={styles.dimHeader}>
|
|
<View style={[styles.dimIcon, { backgroundColor: `${color}20` }]}>
|
|
<Ionicons name="stats-chart-outline" size={20} color={color} />
|
|
</View>
|
|
<View style={styles.dimTitleWrap}>
|
|
<Text style={styles.dimTitle}>{dim.label}</Text>
|
|
<Text style={styles.dimPercentile}>{dim.percentileText}</Text>
|
|
</View>
|
|
<View style={styles.dimScoreWrap}>
|
|
<Text style={styles.dimScore}>{dim.value}</Text>
|
|
<Text style={[styles.dimScoreLabel, { color }]}>良好</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.dimTrack}>
|
|
<View style={[styles.dimFill, { width: `${dim.value}%`, backgroundColor: color }]} />
|
|
</View>
|
|
{dim.subItems && (
|
|
<View style={styles.subItems}>
|
|
{dim.subItems.map((item) => (
|
|
<View key={item.id} style={styles.subItem}>
|
|
<Text style={styles.subItemLabel}>{item.label}</Text>
|
|
<View style={styles.subItemTrack}>
|
|
<View
|
|
style={[
|
|
styles.subItemFill,
|
|
{ width: `${(item.score / item.maxScore) * 100}%`, backgroundColor: color },
|
|
]}
|
|
/>
|
|
</View>
|
|
<Text style={styles.subItemScore}>{item.score}分</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:趋势折线图 ===================== */
|
|
|
|
function TrendChart({ trends }: { trends: AbilityTrendPoint[] }) {
|
|
const width = 320;
|
|
const height = 160;
|
|
const padding = { top: 20, right: 20, bottom: 30, left: 30 };
|
|
const chartW = width - padding.left - padding.right;
|
|
const chartH = height - padding.top - padding.bottom;
|
|
const max = 100;
|
|
const months = trends.map((t) => t.month);
|
|
|
|
const lines = DIM_LABELS.map((label, dimIndex) => {
|
|
const key = (['professional', 'project', 'skill', 'growth'] as const)[dimIndex];
|
|
const pts = trends.map((t, i) => ({
|
|
x: padding.left + (i / (trends.length - 1)) * chartW,
|
|
y: padding.top + (1 - (t[key] / max)) * chartH,
|
|
}));
|
|
const d = pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ');
|
|
return { label, color: DIM_COLORS[dimIndex], d, last: pts[pts.length - 1] };
|
|
});
|
|
|
|
return (
|
|
<View style={styles.chartCard}>
|
|
<Text style={styles.chartTitle}>能力成长趋势</Text>
|
|
<Svg width={width} height={height}>
|
|
{/* 网格线 */}
|
|
{[0, 25, 50, 75, 100].map((v, i) => {
|
|
const y = padding.top + (1 - v / max) * chartH;
|
|
return (
|
|
<Line key={i} x1={padding.left} y1={y} x2={width - padding.right} y2={y} stroke={colors.borderLight} strokeWidth={1} />
|
|
);
|
|
})}
|
|
{/* X 轴标签 */}
|
|
{months.map((m, i) => {
|
|
const x = padding.left + (i / (months.length - 1)) * chartW;
|
|
return (
|
|
<SvgText key={m} x={x} y={height - 8} fill={colors.textHint} fontSize="10" textAnchor="middle">
|
|
{m}
|
|
</SvgText>
|
|
);
|
|
})}
|
|
{/* 折线 */}
|
|
{lines.map((line) => (
|
|
<Path key={line.label} d={line.d} fill="none" stroke={line.color} strokeWidth={2} />
|
|
))}
|
|
{/* 端点 */}
|
|
{lines.map((line) => (
|
|
<Circle key={`dot-${line.label}`} cx={line.last.x} cy={line.last.y} r={3} fill={line.color} />
|
|
))}
|
|
</Svg>
|
|
<View style={styles.legend}>
|
|
{lines.map((line) => (
|
|
<View key={line.label} style={styles.legendItem}>
|
|
<View style={[styles.legendDot, { backgroundColor: line.color }]} />
|
|
<Text style={styles.legendText}>{line.label}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:提升建议 ===================== */
|
|
|
|
function ImprovementCard({ item }: { item: import('../src/types').AbilityImprovement }) {
|
|
return (
|
|
<View style={styles.improveCard}>
|
|
<Ionicons name={item.icon as keyof typeof Ionicons.glyphMap} size={22} color={colors.primary} />
|
|
<View style={styles.improveBody}>
|
|
<Text style={styles.improveTitle}>{item.title}</Text>
|
|
<Text style={styles.improveDesc}>{item.description}</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:四态 ===================== */
|
|
|
|
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="alert-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 AbilityScreen() {
|
|
const router = useRouter();
|
|
const { ability, loading, error, refresh } = useAbilityAssessment();
|
|
|
|
if (loading) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<Header onBack={() => router.back()} onRefresh={refresh} />
|
|
<Loading />
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
if (error || !ability) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<Header onBack={() => router.back()} onRefresh={refresh} />
|
|
<ErrorState onRetry={refresh} />
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<Header onBack={() => router.back()} onRefresh={refresh} />
|
|
<ScrollView contentContainerStyle={styles.body} showsVerticalScrollIndicator={false}>
|
|
<ScoreCard score={ability.overallScore} level={ability.level} percentile={ability.percentile} />
|
|
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>四维能力详解</Text>
|
|
{ability.dimensions.map((dim, i) => (
|
|
<DimensionCard key={dim.label} dim={dim} index={i} />
|
|
))}
|
|
</View>
|
|
|
|
<TrendChart trends={ability.trends} />
|
|
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>针对性提升建议</Text>
|
|
{ability.improvements.length === 0 ? (
|
|
<View style={styles.emptyBox}>
|
|
<Ionicons name="bulb-outline" size={28} color={colors.textHint} />
|
|
<Text style={styles.emptyText}>完成测评后,这里会给出针对性的提升建议</Text>
|
|
</View>
|
|
) : (
|
|
ability.improvements.map((item) => <ImprovementCard key={item.id} item={item} />)
|
|
)}
|
|
</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 },
|
|
scoreCard: {
|
|
flexDirection: 'row',
|
|
backgroundColor: colors.white,
|
|
borderRadius: 14,
|
|
padding: 18,
|
|
marginBottom: 12,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
},
|
|
scoreLeft: { flex: 1 },
|
|
scoreLabel: { fontSize: 14, color: colors.textSecondary },
|
|
scoreRow: { flexDirection: 'row', alignItems: 'center', marginTop: 8 },
|
|
scoreValue: { fontSize: 48, fontWeight: '800', color: colors.textPrimary },
|
|
scoreUnit: { fontSize: 14, color: colors.textSecondary, marginTop: 16, marginRight: 8 },
|
|
levelTag: { borderRadius: 10, paddingHorizontal: 10, paddingVertical: 4 },
|
|
levelText: { fontSize: 12, fontWeight: '700' },
|
|
percentile: { fontSize: 13, color: colors.successText, marginTop: 8 },
|
|
scoreIcon: { justifyContent: 'center' },
|
|
section: { marginTop: 16 },
|
|
emptyBox: {
|
|
backgroundColor: colors.surface,
|
|
borderRadius: 12,
|
|
paddingVertical: 28,
|
|
paddingHorizontal: 16,
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
},
|
|
emptyText: { fontSize: 13, color: colors.textSecondary, textAlign: 'center' },
|
|
sectionTitle: { fontSize: 18, fontWeight: '700', color: colors.textPrimary, marginBottom: 12 },
|
|
dimCard: {
|
|
backgroundColor: colors.white,
|
|
borderRadius: 12,
|
|
padding: 16,
|
|
marginBottom: 10,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
},
|
|
dimHeader: { flexDirection: 'row', alignItems: 'center' },
|
|
dimIcon: {
|
|
width: 36,
|
|
height: 36,
|
|
borderRadius: 18,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
dimTitleWrap: { flex: 1, marginLeft: 10 },
|
|
dimTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary },
|
|
dimPercentile: { fontSize: 12, color: colors.textSecondary, marginTop: 2 },
|
|
dimScoreWrap: { alignItems: 'flex-end' },
|
|
dimScore: { fontSize: 24, fontWeight: '800', color: colors.textPrimary },
|
|
dimScoreLabel: { fontSize: 11, fontWeight: '600', marginTop: 2 },
|
|
dimTrack: {
|
|
height: 8,
|
|
borderRadius: 4,
|
|
backgroundColor: colors.borderLight,
|
|
marginTop: 12,
|
|
overflow: 'hidden',
|
|
},
|
|
dimFill: { height: 8, borderRadius: 4 },
|
|
subItems: { marginTop: 12, gap: 10 },
|
|
subItem: { flexDirection: 'row', alignItems: 'center' },
|
|
subItemLabel: { width: 90, fontSize: 12, color: colors.textSecondary },
|
|
subItemTrack: { flex: 1, height: 6, borderRadius: 3, backgroundColor: colors.borderLight, overflow: 'hidden' },
|
|
subItemFill: { height: 6, borderRadius: 3 },
|
|
subItemScore: { width: 50, fontSize: 12, color: colors.textPrimary, textAlign: 'right' },
|
|
chartCard: {
|
|
backgroundColor: colors.white,
|
|
borderRadius: 12,
|
|
padding: 16,
|
|
marginTop: 16,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
},
|
|
chartTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, marginBottom: 10 },
|
|
legend: { flexDirection: 'row', flexWrap: 'wrap', gap: 12, marginTop: 8 },
|
|
legendItem: { flexDirection: 'row', alignItems: 'center' },
|
|
legendDot: { width: 8, height: 8, borderRadius: 4, marginRight: 4 },
|
|
legendText: { fontSize: 11, color: colors.textSecondary },
|
|
improveCard: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: colors.white,
|
|
borderRadius: 12,
|
|
padding: 14,
|
|
marginBottom: 10,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
},
|
|
improveBody: { flex: 1, marginLeft: 12 },
|
|
improveTitle: { fontSize: 14, fontWeight: '700', color: colors.textPrimary },
|
|
improveDesc: { fontSize: 12, color: colors.textSecondary, marginTop: 2, lineHeight: 18 },
|
|
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 },
|
|
});
|
|
|