import { ScoreDirection, ScoreMetric, ScoreSummary, ScoreThreshold } from '@/lib/assessment-types'; import { getScoringMetadata, MetricDefinition, QuestionnaireScoringMetadata } from '@/lib/scoring-metadata'; import { calculateBigFiveResults, calculateIpipNeoResults, ipipNeoItemsByVersion, } from '@/components/questionnaire/test/private/BigFiveCalculator'; import { calculateHEXACOResults } from '@/components/questionnaire/test/private/HEXACOCalculator'; import { calculateRIASECResults } from '@/components/questionnaire/test/private/RIASECCalculator'; import { calculateSchwartzResults, higherOrderNames, } from '@/components/questionnaire/test/private/SchwartzCalculator'; const fallbackMetadata: QuestionnaireScoringMetadata = { questionnaireVersion: 'unknown-questionnaire-v1', scoreVersion: 'mindscope-raw-v1', min: 0, reverseItems: [], direction: 'trait', highScoreMeaning: '高分代表原始作答总和更高,具体含义需要结合量表说明查看。', thresholds: [], note: '该量表暂以结构化原始分保存,用于保留完整记录与后续复核,不替代量表结果页中的正式解释。', }; const labelMap: Record = { extraversion: '外向性', agreeableness: '宜人性', conscientiousness: '尽责性', emotionalStability: '情绪稳定性', openness: '开放性', neuroticism: '神经质', honestyHumility: '诚实谦逊', emotionality: '情绪性', depression: '抑郁', anxiety: '焦虑', stress: '压力', opennessToChange: '开放变化', conservation: '保守稳定', selfEnhancement: '自我提升', selfTranscendence: '自我超越', R: '现实型', I: '研究型', A: '艺术型', S: '社会型', E: '企业型', C: '常规型', total: '总分', raw: '原始作答总和', }; function sum(answers: string[]) { return answers.reduce((total, answer) => total + (Number(answer) || 0), 0); } function keyed(answers: string[]) { return Object.fromEntries(answers.map((answer, index) => [index + 1, answer])); } function metadataFor(questionnaireId: string) { return getScoringMetadata(questionnaireId) || { ...fallbackMetadata, questionnaireVersion: `${questionnaireId || 'unknown'}-v1`, }; } function thresholdLabel(value: number, thresholds: ScoreThreshold[]) { if (!thresholds.length) return undefined; return thresholds.reduce((current, threshold) => ( value >= threshold.min ? threshold.label : current ), thresholds[0].label); } function metricDefinition(meta: QuestionnaireScoringMetadata, key: string): MetricDefinition | undefined { return meta.metrics?.find((item) => item.key === key); } function makeMetric( meta: QuestionnaireScoringMetadata, key: string, label: string, value: number, overrides: Partial = {}, ): ScoreMetric { const definition = metricDefinition(meta, key); const min = overrides.min ?? definition?.min ?? meta.min; const max = overrides.max ?? definition?.max ?? meta.max; const thresholds = overrides.thresholds ?? definition?.thresholds ?? meta.thresholds; return { key, label, value, min, max, direction: overrides.direction ?? definition?.direction ?? meta.direction, highScoreMeaning: overrides.highScoreMeaning ?? definition?.highScoreMeaning ?? meta.highScoreMeaning, thresholds, level: overrides.level ?? thresholdLabel(value, thresholds), }; } function finalize( questionnaireId: string, metrics: ScoreMetric[], primary?: ScoreMetric, options: { note?: string; scoringStatus?: ScoreSummary['scoringStatus'] } = {}, ): ScoreSummary { const meta = metadataFor(questionnaireId); return { primary, metrics, note: options.note ?? meta.note, questionnaireVersion: meta.questionnaireVersion, scoreVersion: meta.scoreVersion, min: meta.min, max: meta.max, reverseItems: meta.reverseItems, direction: meta.direction, highScoreMeaning: meta.highScoreMeaning, thresholds: meta.thresholds, scoringStatus: options.scoringStatus ?? 'structured', }; } export function buildScoreSummary(questionnaireId: string, answers: string[]): ScoreSummary { const meta = metadataFor(questionnaireId); if (questionnaireId === 'bigfive') { const result = calculateBigFiveResults(answers); const metrics = Object.entries(result).map(([key, value]) => ( makeMetric(meta, key, labelMap[key] || key, value.score, { min: 10, max: 50 }) )); return finalize(questionnaireId, metrics); } if (questionnaireId === 'bigfive-120' || questionnaireId === 'bigfive-300') { const version = questionnaireId === 'bigfive-120' ? 120 : 300; const result = calculateIpipNeoResults(answers, [...ipipNeoItemsByVersion[version]]); const perDomainMax = version === 120 ? 120 : 300; const metrics = Object.entries(result.domains).map(([key, value]) => ( makeMetric(meta, key, labelMap[key] || key, value.score, { min: value.itemCount, max: perDomainMax, }) )); return finalize(questionnaireId, metrics); } if (questionnaireId === 'hexaco') { const metrics = Object.entries(calculateHEXACOResults(answers)).map(([key, value]) => ( makeMetric(meta, key, labelMap[key] || key, Number(value.toFixed(2)), { min: 1, max: 5 }) )); return finalize(questionnaireId, metrics); } if (questionnaireId === 'riasec') { const result = calculateRIASECResults(answers); const metrics = Object.entries(result.scores).map(([key, value]) => ( makeMetric(meta, key, labelMap[key] || key, value.score, { min: 0, max: 40 }) )); const primary = makeMetric(meta, 'holland', `霍兰德代码:${result.hollandCode}`, result.ranking[0][1].score, { min: 0, max: 40, }); return finalize(questionnaireId, metrics, primary); } if (questionnaireId === 'schwartz') { const result = calculateSchwartzResults(answers); const metrics = Object.entries(result.higherOrderScores).map(([key, value]) => ( makeMetric(meta, key, higherOrderNames[key as keyof typeof higherOrderNames] || labelMap[key] || key, Number(value.toFixed(2)), { min: 1, max: 5, }) )); return finalize(questionnaireId, metrics); } if (questionnaireId === 'dass21') { const answerMap = keyed(answers); const groups = { depression: [3, 5, 10, 13, 16, 17, 21], anxiety: [2, 4, 7, 9, 15, 19, 20], stress: [1, 6, 8, 11, 12, 14, 18], }; const metrics = Object.entries(groups).map(([key, ids]) => { const value = ids.reduce((total, id) => total + Number(answerMap[id] || 0), 0) * 2; return makeMetric(meta, key, labelMap[key] || key, value, { min: 0, max: 42 }); }); return finalize(questionnaireId, metrics); } if (questionnaireId === 'pss10') { const reverse = new Set([4, 5, 7, 8]); const score = answers.reduce((total, answer, index) => { const value = Number(answer) || 0; return total + (reverse.has(index + 1) ? 4 - value : value); }, 0); const primary = makeMetric(meta, 'total', 'PSS-10 总分', score, { min: 0, max: 40 }); return finalize(questionnaireId, [primary], primary); } if (questionnaireId === 'sds') { const reverse = new Set([2, 5, 6, 11, 12, 14, 16, 17, 18, 20]); const original = answers.reduce((total, answer, index) => { const value = Number(answer) || 0; return total + (reverse.has(index + 1) ? 5 - value : value); }, 0); const score = Math.round(original * 1.25); const primary = makeMetric(meta, 'total', 'SDS 标准分', score, { min: 25, max: 100 }); return finalize(questionnaireId, [primary], primary); } const raw = sum(answers); const single: Record = { phq9: { label: 'PHQ-9 总分', value: raw, max: 27 }, gad7: { label: 'GAD-7 总分', value: raw, max: 21 }, isi: { label: 'ISI 总分', value: raw, max: 28 }, bdi2: { label: 'BDI-II 总分', value: raw, max: 63 }, who5: { label: 'WHO-5 百分制得分', value: raw * 4, max: 100 }, crt: { label: 'CRT 答对题数', value: raw, max: 7 }, }; if (single[questionnaireId]) { const item = single[questionnaireId]; const primary = makeMetric(meta, 'total', item.label, item.value, { min: meta.min, max: item.max ?? meta.max }); return finalize(questionnaireId, [primary], primary); } const primary = makeMetric(meta, 'raw', '原始作答总和', raw, { min: meta.min, max: meta.max, direction: meta.direction as ScoreDirection, }); return finalize(questionnaireId, [primary], primary, { scoringStatus: 'raw' }); }