import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { Alert, Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import Svg, { Line, Polygon, Text as SvgText } from 'react-native-svg'; import { useInterviewScore } from '../src/hooks'; import { LoadingState, ErrorState } from '../src/components/StateView'; import {colors, semanticColorTokens, spacing} from '../src/theme'; import { formatDateTime } from '../src/utils'; import type { HireSuggestion, InterviewScoreData, InterviewScoreDimension, InterviewScoreTag } from '../src/types'; /* ===================== 子组件:顶部导航 ===================== */ function Header({ onBack }: { onBack: () => void }) { return ( 面试评分 {/* 原先这里是「历史评分」入口:没有历史评分页,先摘掉,加页面时再挂回来 */} ); } /* ===================== 子组件:面试信息卡 ===================== */ function InterviewInfoCard({ data }: { data: InterviewScoreData }) { return ( 候选人 {data.candidateName} 面试岗位 {data.positionName} 面试时间 {data.interviewTime} ); } /* ===================== 子组件:综合评分卡 ===================== */ function OverallScoreCard({ score }: { score: number }) { return ( 综合评分 {score} ); } /* ===================== 子组件:雷达图 ===================== */ function RadarChart({ dimensions }: { dimensions: InterviewScoreDimension[] }) { const size = 220; const center = size / 2; const radius = 80; const angleStep = (Math.PI * 2) / dimensions.length; const axisPoints = dimensions.map((_, i) => { const angle = i * angleStep - Math.PI / 2; return { x: center + radius * Math.cos(angle), y: center + radius * Math.sin(angle), label: dimensions[i].name, }; }); const dataPoints = dimensions.map((d, i) => { const angle = i * angleStep - Math.PI / 2; const r = (d.score / d.maxScore) * radius; return { x: center + r * Math.cos(angle), y: center + r * Math.sin(angle), }; }); const polygonPoints = dataPoints.map((p) => `${p.x},${p.y}`).join(' '); return ( 八维能力评分 {[0.2, 0.4, 0.6, 0.8, 1].map((scale) => ( { const px = center + (a.x - center) * scale; const py = center + (a.y - center) * scale; return `${px},${py}`; }).join(' ')} fill="none" stroke={colors.borderLight} strokeWidth={1} /> ))} {axisPoints.map((a, i) => ( ))} {axisPoints.map((a, i) => { const labelX = center + (a.x - center) * 1.18; const labelY = center + (a.y - center) * 1.18; return ( {a.label} ); })} {dimensions.map((d) => ( {d.name} {d.score} ))} ); } /* ===================== 子组件:评价标签 ===================== */ function TagGroup({ tags, onToggle }: { tags: InterviewScoreTag[]; onToggle: (id: string) => void }) { return ( 面试评价标签 {tags.map((tag) => { const color = semanticColorTokens[tag.color] ?? colors.primary; return ( onToggle(tag.id)} > {tag.selected && } {tag.label} ); })} ); } /* ===================== 子组件:文字评价 ===================== */ function CommentInput({ value, onChangeText }: { value: string; onChangeText: (text: string) => void }) { return ( 文字评价 {value.length}/500 ); } /* ===================== 子组件:录用建议 ===================== */ function HireSuggestionGroup({ suggestions, onSelect, }: { suggestions: HireSuggestion[]; onSelect: (type: HireSuggestion['type']) => void; }) { const typeColor: Record = { strong: colors.success, consider: colors.compareOrange, reject: colors.danger, }; return ( 录用建议 {suggestions.map((s) => ( onSelect(s.type)} > {s.title} {s.desc} {s.selected && } ))} ); } /* ===================== 子组件:薪资建议 ===================== */ function SalarySuggestionCard({ salary }: { salary: InterviewScoreData['salarySuggestion'] }) { return ( 薪资建议(选填) {salary.range} {salary.level} {salary.note} ); } /* ===================== 页面主组件 ===================== */ export default function InterviewScoreScreen() { const router = useRouter(); const { score, loading, error, toggleTag, updateComment, selectHireSuggestion, submit, submitting, saveDraft, savingDraft, restoreDraft, discardDraft, hasPendingDraft, draftSavedAt, refresh, } = useInterviewScore('interview-1'); /** 保存草稿:成功后就地在按钮旁显示保存时间,失败给出提示 */ const handleSaveDraft = async () => { try { const savedAt = await saveDraft(); if (savedAt) { Alert.alert('已保存草稿', `保存时间 ${formatDateTime(savedAt)},下次进入这场面试时可恢复。`); } } catch { Alert.alert('保存失败', '草稿没能存到本机,请稍后重试。'); } }; if (loading) { return ( ); } if (error || !score) { return ( ); } return (
router.back()} /> {hasPendingDraft && ( 发现未提交的草稿{draftSavedAt ? `(${formatDateTime(draftSavedAt)} 保存)` : ''} 恢复后会覆盖当前页面上的评分内容 恢复 void discardDraft()} hitSlop={6}> 放弃 )} {savingDraft ? '保存中…' : '保存草稿'} {submitting ? '提交中...' : '提交评分'} ); } /* ===================== 样式 ===================== */ const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.pageBg, }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: colors.pageBg, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: spacing.framework, paddingVertical: 12, backgroundColor: colors.white, borderBottomWidth: 1, borderBottomColor: colors.borderLight, }, headerBtn: { width: 36, height: 36, justifyContent: 'center', alignItems: 'center', }, headerTitle: { fontSize: 17, fontWeight: '600', color: colors.textPrimary, }, scroll: { flex: 1, }, scrollContent: { padding: spacing.card, paddingBottom: 100, }, infoCard: { backgroundColor: colors.white, borderRadius: spacing.card, padding: spacing.card, marginBottom: 8, }, infoRow: { flexDirection: 'row', marginBottom: 12, }, infoItem: { flex: 1, }, infoLabel: { fontSize: 12, color: colors.textHint, marginBottom: 4, }, infoValue: { fontSize: 15, fontWeight: '600', color: colors.textPrimary, }, scoreCard: { backgroundColor: colors.primary, borderRadius: spacing.card, padding: spacing.card, marginBottom: 8, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, scoreLabel: { fontSize: 14, color: colors.white, opacity: 0.9, }, scoreValue: { fontSize: 42, fontWeight: '700', color: colors.white, marginTop: 4, }, scoreIcon: { width: 64, height: 64, borderRadius: 32, backgroundColor: colors.onBrandSurface, justifyContent: 'center', alignItems: 'center', }, card: { backgroundColor: colors.white, borderRadius: spacing.card, padding: spacing.card, marginBottom: 8, }, cardTitle: { fontSize: 16, fontWeight: '700', color: colors.textPrimary, marginBottom: 12, }, radarWrap: { alignItems: 'center', }, dimensionList: { marginTop: 16, }, dimensionItem: { flexDirection: 'row', alignItems: 'center', marginBottom: 10, }, dimensionName: { width: 70, fontSize: 13, color: colors.textSecondary, }, dimensionTrack: { flex: 1, height: 8, borderRadius: 4, backgroundColor: colors.borderLight, marginHorizontal: 10, }, dimensionFill: { height: 8, borderRadius: 4, backgroundColor: colors.primary, }, dimensionScore: { width: 28, fontSize: 13, fontWeight: '600', color: colors.textPrimary, textAlign: 'right', }, tagRow: { flexDirection: 'row', flexWrap: 'wrap', marginHorizontal: -4, }, tag: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, paddingVertical: 7, borderRadius: 18, borderWidth: 1, borderColor: colors.border, margin: 4, backgroundColor: colors.white, }, tagText: { fontSize: 13, color: colors.textPrimary, marginLeft: 4, }, tagTextActive: { color: colors.white, fontWeight: '500', }, textarea: { height: 120, borderWidth: 1, borderColor: colors.border, borderRadius: 12, padding: 12, fontSize: 14, color: colors.textPrimary, backgroundColor: colors.pageBg, }, charCount: { textAlign: 'right', fontSize: 12, color: colors.textHint, marginTop: 8, }, suggestionRow: { flexDirection: 'row', alignItems: 'center', padding: spacing.card, borderRadius: spacing.card, borderWidth: 1, borderColor: colors.borderLight, marginBottom: 8, }, suggestionDot: { width: 10, height: 10, borderRadius: 5, marginRight: 10, }, suggestionBody: { flex: 1, }, suggestionTitle: { fontSize: 14, fontWeight: '600', color: colors.textPrimary, marginBottom: 2, }, suggestionDesc: { fontSize: 12, color: colors.textSecondary, }, salaryBox: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', backgroundColor: colors.infoLight, borderRadius: spacing.card, padding: spacing.card, }, salaryRange: { fontSize: 24, fontWeight: '700', color: colors.info, }, salaryLevel: { fontSize: 14, color: colors.infoText, marginTop: 4, }, salaryNote: { fontSize: 12, color: colors.textSecondary, marginTop: 10, }, footer: { flexDirection: 'row', padding: spacing.framework, paddingBottom: 24, backgroundColor: colors.white, borderTopWidth: 1, borderTopColor: colors.borderLight, }, draftBtn: { flex: 1, height: 46, borderRadius: 24, borderWidth: 1, borderColor: colors.border, justifyContent: 'center', alignItems: 'center', marginRight: 12, }, draftText: { fontSize: 15, fontWeight: '600', color: colors.textPrimary, }, draftBtnDisabled: { opacity: 0.6 }, draftBanner: { flexDirection: 'row', alignItems: 'center', gap: 10, backgroundColor: colors.infoLight, borderWidth: 1, borderColor: colors.infoBorder, borderRadius: spacing.card, paddingHorizontal: spacing.card, paddingVertical: spacing.card, marginBottom: 8, }, draftBannerBody: { flex: 1 }, draftBannerTitle: { fontSize: 13, fontWeight: '700', color: colors.infoText }, draftBannerDesc: { fontSize: 11, color: colors.textSecondary, marginTop: 2 }, draftBannerBtn: { backgroundColor: colors.infoBtn, borderRadius: 14, paddingHorizontal: 12, paddingVertical: 6, }, draftBannerBtnText: { fontSize: 12, fontWeight: '700', color: colors.white }, draftBannerDiscard: { fontSize: 12, color: colors.textSecondary }, submitBtn: { flex: 2, height: 46, borderRadius: 24, backgroundColor: colors.primaryBtn, justifyContent: 'center', alignItems: 'center', }, submitBtnDisabled: { opacity: 0.6, }, submitText: { fontSize: 15, fontWeight: '600', color: colors.white, }, });