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.
470 lines
15 KiB
470 lines
15 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 { useHREvaluation } from '../src/hooks';
|
|
import { colors } from '../src/theme';
|
|
import type { EvaluationDimension, EvaluationReview, EvaluationTrendPoint } from '../src/types';
|
|
|
|
/** 评级 → 文字颜色(优秀=品牌蓝,良好=主色,其他=次要文字) */
|
|
const LEVEL_TEXT_COLOR: Record<string, string> = {
|
|
优秀: colors.primaryDark,
|
|
良好: colors.primary,
|
|
中等: colors.textSecondary,
|
|
};
|
|
|
|
/* ===================== 子组件:顶部导航 ===================== */
|
|
|
|
function Header({ onBack, onAppeal }: { onBack: () => void; onAppeal: () => 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}>HR评价详情</Text>
|
|
<Pressable style={styles.headerAppealBtn} onPress={onAppeal} hitSlop={8}>
|
|
<Text style={styles.headerAppealText}>申诉</Text>
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:综合评分卡 ===================== */
|
|
|
|
function ScoreCard({
|
|
score,
|
|
percentile,
|
|
change,
|
|
}: {
|
|
score: number;
|
|
percentile: number;
|
|
change: number;
|
|
}) {
|
|
return (
|
|
<View style={styles.scoreCard}>
|
|
<View style={styles.scoreLeft}>
|
|
<Text style={styles.scoreLabel}>综合评分</Text>
|
|
<View style={styles.scoreValueRow}>
|
|
<Text style={styles.scoreValue}>{score}</Text>
|
|
<Text style={styles.scoreUnit}>分</Text>
|
|
<View style={styles.levelBadge}>
|
|
<Text style={styles.levelText}>良好</Text>
|
|
</View>
|
|
</View>
|
|
<Text style={styles.scoreMeta}>
|
|
超过 {percentile}% 的求职者
|
|
<Text style={change >= 0 ? styles.changeUp : styles.changeDown}>
|
|
{' '}
|
|
较上期 {change >= 0 ? '+' : ''}
|
|
{change}
|
|
</Text>
|
|
</Text>
|
|
</View>
|
|
<View style={styles.scoreIcon}>
|
|
<Ionicons name="ribbon-outline" size={40} color={colors.primary} />
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:评分趋势折线图 ===================== */
|
|
|
|
function TrendChart({ trend }: { trend: EvaluationTrendPoint[] }) {
|
|
const width = 320;
|
|
const height = 150;
|
|
const padding = { top: 20, right: 20, bottom: 28, left: 32 };
|
|
const chartW = width - padding.left - padding.right;
|
|
const chartH = height - padding.top - padding.bottom;
|
|
const max = 100;
|
|
const min = 75;
|
|
|
|
const points = trend.map((t, i) => ({
|
|
x: padding.left + (i / (trend.length - 1)) * chartW,
|
|
y: padding.top + (1 - (t.score - min) / (max - min)) * chartH,
|
|
score: t.score,
|
|
month: t.month,
|
|
}));
|
|
|
|
const d = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ');
|
|
const last = points[points.length - 1];
|
|
|
|
return (
|
|
<View style={styles.chartCard}>
|
|
<Text style={styles.cardTitle}>评分趋势</Text>
|
|
<Svg width={width} height={height}>
|
|
{[80, 85, 90, 95, 100].map((v) => {
|
|
const y = padding.top + (1 - (v - min) / (max - min)) * chartH;
|
|
return (
|
|
<Line
|
|
key={v}
|
|
x1={padding.left}
|
|
y1={y}
|
|
x2={width - padding.right}
|
|
y2={y}
|
|
stroke={colors.borderLight}
|
|
strokeWidth={1}
|
|
/>
|
|
);
|
|
})}
|
|
{[80, 90, 100].map((v) => {
|
|
const y = padding.top + (1 - (v - min) / (max - min)) * chartH;
|
|
return (
|
|
<SvgText key={`label-${v}`} x={padding.left - 6} y={y + 3} fill={colors.textHint} fontSize="10" textAnchor="end">
|
|
{v}
|
|
</SvgText>
|
|
);
|
|
})}
|
|
<Path d={d} fill="none" stroke={colors.primary} strokeWidth={2} />
|
|
{points.map((p, i) => (
|
|
<Circle key={`dot-${i}`} cx={p.x} cy={p.y} r={3} fill={colors.primary} />
|
|
))}
|
|
<Circle cx={last.x} cy={last.y} r={5} fill={colors.primary} />
|
|
{points.map((p) => (
|
|
<SvgText
|
|
key={`month-${p.month}`}
|
|
x={p.x}
|
|
y={height - 8}
|
|
fill={colors.textHint}
|
|
fontSize="10"
|
|
textAnchor="middle"
|
|
>
|
|
{p.month}
|
|
</SvgText>
|
|
))}
|
|
</Svg>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:六维能力评分 ===================== */
|
|
|
|
function DimensionCard({ dimensions }: { dimensions: EvaluationDimension[] }) {
|
|
return (
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>六维能力评分</Text>
|
|
{dimensions.map((dim) => (
|
|
<View key={dim.id} style={styles.dimRow}>
|
|
<View style={styles.dimHead}>
|
|
<Text style={styles.dimName}>{dim.name}</Text>
|
|
<View style={styles.dimRight}>
|
|
<Text style={styles.dimScore}>{dim.score}</Text>
|
|
<View style={styles.dimTag}>
|
|
<Text
|
|
style={[
|
|
styles.dimTagText,
|
|
{ color: LEVEL_TEXT_COLOR[dim.level] ?? colors.textSecondary },
|
|
]}
|
|
>
|
|
{dim.level}
|
|
</Text>
|
|
</View>
|
|
{dim.change !== undefined && (
|
|
<Text style={dim.change >= 0 ? styles.changeUp : styles.changeDown}>
|
|
{dim.change >= 0 ? '+' : ''}
|
|
{dim.change}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
<View style={styles.dimTrack}>
|
|
<View style={[styles.dimFill, { width: `${dim.score}%` }]} />
|
|
</View>
|
|
</View>
|
|
))}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:HR 评价列表 ===================== */
|
|
|
|
function ReviewCard({ review }: { review: EvaluationReview }) {
|
|
return (
|
|
<View style={styles.reviewCard}>
|
|
<View style={styles.reviewHead}>
|
|
<Text style={styles.reviewCompany}>{review.company}</Text>
|
|
<View style={styles.reviewRating}>
|
|
<Ionicons name="star" size={14} color={colors.action} />
|
|
<Text style={styles.reviewRatingText}>{review.rating}.0</Text>
|
|
</View>
|
|
</View>
|
|
<Text style={styles.reviewDate}>{review.date}</Text>
|
|
<Text style={styles.reviewContent}>{review.content}</Text>
|
|
<View style={styles.reviewTags}>
|
|
{review.tags.map((tag) => (
|
|
<View key={tag} style={styles.reviewTag}>
|
|
<Text style={styles.reviewTagText}>{tag}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
/* ===================== 子组件:评价说明 ===================== */
|
|
|
|
function RulesCard({ rules }: { rules: string[] }) {
|
|
return (
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>评价说明</Text>
|
|
{rules.map((rule, i) => (
|
|
<View key={i} style={styles.ruleRow}>
|
|
<View style={styles.ruleDot} />
|
|
<Text style={styles.ruleText}>{rule}</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="ribbon-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 HREvaluationScreen() {
|
|
const router = useRouter();
|
|
const { evaluation, loading, error, refresh } = useHREvaluation();
|
|
|
|
if (loading) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<Header onBack={() => router.back()} onAppeal={() => {}} />
|
|
<Loading />
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
if (error || !evaluation) {
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<Header onBack={() => router.back()} onAppeal={() => {}} />
|
|
<ErrorState onRetry={refresh} />
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|
<Header onBack={() => router.back()} onAppeal={() => router.push('/appeal')} />
|
|
<ScrollView contentContainerStyle={styles.body} showsVerticalScrollIndicator={false}>
|
|
<ScoreCard
|
|
score={evaluation.overallScore}
|
|
percentile={evaluation.percentile}
|
|
change={3.2}
|
|
/>
|
|
<TrendChart trend={evaluation.trend} />
|
|
<DimensionCard dimensions={evaluation.dimensions} />
|
|
|
|
<View style={styles.sectionHeader}>
|
|
<Text style={styles.sectionTitle}>HR评价({evaluation.reviews.length})</Text>
|
|
</View>
|
|
{evaluation.reviews.map((review) => (
|
|
<ReviewCard key={review.id} review={review} />
|
|
))}
|
|
|
|
<RulesCard rules={evaluation.rules} />
|
|
</ScrollView>
|
|
|
|
<View style={styles.footer}>
|
|
<Text style={styles.footerQuestion}>对评分有疑问?</Text>
|
|
<Pressable style={styles.footerBtn} onPress={() => router.push('/appeal')}>
|
|
<Text style={styles.footerBtnText}>发起申诉</Text>
|
|
</Pressable>
|
|
</View>
|
|
</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 },
|
|
headerAppealBtn: {
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 5,
|
|
borderRadius: 14,
|
|
backgroundColor: colors.primaryLight,
|
|
},
|
|
headerAppealText: { fontSize: 13, fontWeight: '600', color: colors.primaryText },
|
|
body: { paddingHorizontal: 16, paddingBottom: 100, paddingTop: 12 },
|
|
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 },
|
|
scoreCard: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
backgroundColor: colors.white,
|
|
borderRadius: 14,
|
|
padding: 18,
|
|
marginBottom: 12,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
},
|
|
scoreLeft: { flex: 1 },
|
|
scoreLabel: { fontSize: 13, color: colors.textSecondary },
|
|
scoreValueRow: { flexDirection: 'row', alignItems: 'center', marginTop: 8 },
|
|
scoreValue: { fontSize: 44, fontWeight: '800', color: colors.textPrimary },
|
|
scoreUnit: { fontSize: 14, color: colors.textSecondary, marginTop: 16, marginLeft: 4 },
|
|
levelBadge: {
|
|
marginLeft: 10,
|
|
backgroundColor: colors.primaryLight,
|
|
borderRadius: 10,
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 4,
|
|
},
|
|
levelText: { fontSize: 12, fontWeight: '700', color: colors.primaryDark },
|
|
scoreMeta: { fontSize: 13, color: colors.successText, marginTop: 10 },
|
|
changeUp: { color: colors.successText },
|
|
changeDown: { color: colors.danger },
|
|
scoreIcon: { alignItems: 'center', justifyContent: 'center' },
|
|
card: {
|
|
backgroundColor: colors.white,
|
|
borderRadius: 14,
|
|
padding: 16,
|
|
marginBottom: 12,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
},
|
|
chartCard: {
|
|
backgroundColor: colors.white,
|
|
borderRadius: 14,
|
|
padding: 16,
|
|
marginBottom: 12,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
},
|
|
cardTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, marginBottom: 12 },
|
|
dimRow: { marginBottom: 14 },
|
|
dimHead: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
marginBottom: 6,
|
|
},
|
|
dimName: { fontSize: 14, color: colors.textPrimary },
|
|
dimRight: { flexDirection: 'row', alignItems: 'center' },
|
|
dimScore: { fontSize: 14, fontWeight: '700', color: colors.textPrimary, marginRight: 8 },
|
|
dimTag: {
|
|
backgroundColor: colors.pageBg,
|
|
borderRadius: 8,
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 2,
|
|
marginRight: 8,
|
|
},
|
|
dimTagText: { fontSize: 11, fontWeight: '600' },
|
|
dimTrack: {
|
|
height: 8,
|
|
borderRadius: 4,
|
|
backgroundColor: colors.borderLight,
|
|
overflow: 'hidden',
|
|
},
|
|
dimFill: { height: 8, borderRadius: 4, backgroundColor: colors.primary },
|
|
sectionHeader: { marginBottom: 10, marginTop: 4 },
|
|
sectionTitle: { fontSize: 16, fontWeight: '700', color: colors.textPrimary },
|
|
reviewCard: {
|
|
backgroundColor: colors.white,
|
|
borderRadius: 12,
|
|
padding: 14,
|
|
marginBottom: 10,
|
|
borderWidth: 1,
|
|
borderColor: colors.borderLight,
|
|
},
|
|
reviewHead: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
},
|
|
reviewCompany: { fontSize: 15, fontWeight: '700', color: colors.textPrimary },
|
|
reviewRating: { flexDirection: 'row', alignItems: 'center' },
|
|
reviewRatingText: {
|
|
fontSize: 13,
|
|
fontWeight: '700',
|
|
color: colors.actionText,
|
|
marginLeft: 4,
|
|
},
|
|
reviewDate: { fontSize: 12, color: colors.textHint, marginTop: 2 },
|
|
reviewContent: { fontSize: 13, color: colors.textSecondary, marginTop: 10, lineHeight: 20 },
|
|
reviewTags: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 10 },
|
|
reviewTag: {
|
|
backgroundColor: colors.primaryLight,
|
|
borderRadius: 8,
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 2,
|
|
},
|
|
reviewTagText: { fontSize: 11, color: colors.primaryText },
|
|
ruleRow: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 10 },
|
|
ruleDot: {
|
|
width: 5,
|
|
height: 5,
|
|
borderRadius: 3,
|
|
backgroundColor: colors.primary,
|
|
marginTop: 7,
|
|
marginRight: 8,
|
|
},
|
|
ruleText: { flex: 1, fontSize: 13, color: colors.textSecondary, lineHeight: 20 },
|
|
footer: {
|
|
position: 'absolute',
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 12,
|
|
backgroundColor: colors.white,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.borderLight,
|
|
},
|
|
footerQuestion: { fontSize: 14, color: colors.textPrimary },
|
|
footerBtn: {
|
|
backgroundColor: colors.primaryBtn,
|
|
borderRadius: 18,
|
|
paddingHorizontal: 20,
|
|
paddingVertical: 9,
|
|
},
|
|
footerBtnText: { fontSize: 14, fontWeight: '700', color: colors.white },
|
|
});
|
|
|