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 } 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 (
八维能力评分
{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 (
{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: 16,
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: 8,
paddingBottom: 100,
},
infoCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
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: 8,
padding: 8,
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: 8,
padding: 8,
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: 8,
borderRadius: 8,
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: 8,
padding: 8,
},
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: 16,
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: 8,
paddingHorizontal: 8,
paddingVertical: 8,
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,
},
});