feat: 完善中文心理测评平台

This commit is contained in:
mikemoi
2026-06-22 22:59:01 +02:00
commit 9227c687fc
160 changed files with 16974 additions and 0 deletions
@@ -0,0 +1,134 @@
'use client';
import React from 'react';
import { useScopedI18n } from '@/locales/client';
import { calculateADHDResults } from '../../test/private/ADHDCalculator';
function useLabels() {
const t = useScopedI18n('components.adhdResult');
return {
totalScore: t('totalScore'),
inattention: t('inattention'),
hyperactivity: t('hyperactivity'),
partAScore: t('partAScore'),
screeningResult: t('screeningResult'),
severityLevel: t('severityLevel'),
positiveScreen: t('positiveScreen'),
negativeScreen: t('negativeScreen'),
partAPositiveResponses: t('partAPositiveResponses'),
basedOnTotalScore: t('basedOnTotalScore'),
recommendations: t('recommendations'),
importantNotes: t('importantNotes'),
severityLevels: {
low: t('severityLevels.low'),
mild: t('severityLevels.mild'),
moderate: t('severityLevels.moderate'),
high: t('severityLevels.high'),
},
notes: {
screening: t('notes.screening'),
symptoms: t('notes.symptoms'),
evaluation: t('notes.evaluation'),
},
recommendationTexts: {
positive: t('recommendationTexts.positive'),
negative: t('recommendationTexts.negative'),
},
};
}
export function ADHDResult({
answers,
}: {
answers: string[];
}) {
const labels = useLabels();
// Convert answers array to object format expected by calculator
const answersObj: { [key: number]: string } = {};
answers.forEach((answer, index) => {
answersObj[index + 1] = answer;
});
const results = calculateADHDResults({ answers: answersObj, questions: [] });
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'low': return 'text-green-600';
case 'mild': return 'text-yellow-600';
case 'moderate': return 'text-orange-600';
case 'high': return 'text-red-600';
default: return 'text-gray-600';
}
};
const getSeverityLabel = (severity: string) => {
return labels.severityLevels[severity as keyof typeof labels.severityLevels] || 'Unknown';
};
return (
<div className="mt-6 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<MetricCard title={labels.totalScore} value={results.totalScore} />
<MetricCard title={labels.inattention} value={results.factorScores.inattention} />
<MetricCard title={labels.hyperactivity} value={results.factorScores.hyperactivity} />
<MetricCard title={labels.partAScore} value={results.factorScores.partA} />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white border rounded-lg p-4 shadow-sm">
<h3 className="text-sm font-medium text-gray-500 mb-2">{labels.screeningResult}</h3>
<div className={`text-lg font-semibold ${results.screeningPositive ? 'text-orange-600' : 'text-green-600'}`}>
{results.screeningPositive ? labels.positiveScreen : labels.negativeScreen}
</div>
<p className="text-sm text-gray-600 mt-1">
{labels.partAPositiveResponses}: {results.partAPositive}/6
</p>
</div>
<div className="bg-white border rounded-lg p-4 shadow-sm">
<h3 className="text-sm font-medium text-gray-500 mb-2">{labels.severityLevel}</h3>
<div className={`text-lg font-semibold ${getSeverityColor(results.severity)}`}>
{getSeverityLabel(results.severity)}
</div>
<p className="text-sm text-gray-600 mt-1">
{labels.basedOnTotalScore}: {results.totalScore}/72
</p>
</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 className="text-sm font-medium text-blue-800 mb-2">{labels.recommendations}</h3>
<p className="text-sm text-blue-700">
{results.screeningPositive ? labels.recommendationTexts.positive : labels.recommendationTexts.negative}
</p>
</div>
<div className="bg-gray-50 border rounded-lg p-4">
<h3 className="text-sm font-medium text-gray-700 mb-2">{labels.importantNotes}</h3>
<ul className="text-sm text-gray-600 space-y-1">
<li> {labels.notes.screening}</li>
<li> {labels.notes.symptoms}</li>
<li> {labels.notes.evaluation}</li>
</ul>
</div>
</div>
);
}
interface MetricCardProps {
title: string;
value: React.ReactNode;
className?: string;
}
function MetricCard({ title, value, className = '' }: MetricCardProps) {
return (
<div
className={`bg-white border rounded-lg p-4 flex flex-col items-center shadow-sm ${className}`}
>
<span className="text-sm text-gray-500 mb-1">{title}</span>
<span className="text-2xl font-semibold text-indigo-600">{value}</span>
</div>
);
}
@@ -0,0 +1,84 @@
'use client';
import { calculateAttachmentResults } from '../../test/private/AttachmentCalculator';
interface AttachmentResultProps {
answers: string[];
}
const patternText = {
secure: '相对安全型',
preoccupied: '焦虑偏高型',
dismissive: '回避偏高型',
fearful: '焦虑与回避都偏高',
};
const patternDescription = {
secure: '你在当前重要关系中通常较能亲近、信任和表达需求,不安全感相对较低。',
preoccupied: '你可能较容易担心关系不够稳定,或需要更多确认来获得安全感。',
dismissive: '你可能更习惯保持独立和距离,不太愿意依靠他人或表达脆弱。',
fearful: '你可能既渴望亲近,又担心受伤或被拒绝,因此在关系中容易拉扯。',
};
function width(value: number) {
return `${Math.max(0, Math.min(100, ((value - 1) / 6) * 100))}%`;
}
export function AttachmentResult({ answers }: AttachmentResultProps) {
const results = calculateAttachmentResults(answers);
return (
<div className="mt-6 space-y-6">
<div className="border bg-white p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<MetricCard title="依恋回避" value={`${results.avoidance.toFixed(2)}/7`} />
<MetricCard title="依恋焦虑" value={`${results.anxiety.toFixed(2)}/7`} />
<MetricCard title="模式提示" value={patternText[results.pattern]} />
</div>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<BarCard title="依恋回避" value={results.avoidance} />
<BarCard title="依恋焦虑" value={results.anxiety} />
</div>
<div className="border bg-blue-50 p-6 text-blue-900 shadow-sm">
<h3 className="text-lg font-semibold mb-3"></h3>
<p className="text-sm">{patternDescription[results.pattern]}</p>
</div>
<div className="border bg-gray-50 p-4 text-sm text-gray-700">
</div>
</div>
);
}
function MetricCard({ title, value }: { title: string; value: string }) {
return (
<div className="rounded-lg bg-gray-50 p-4 text-center">
<div className="text-sm text-muted-foreground">{title}</div>
<div className="mt-1 text-2xl font-semibold text-indigo-600">{value}</div>
</div>
);
}
function BarCard({ title, value }: { title: string; value: number }) {
return (
<div className="rounded-lg border bg-white p-5 shadow-sm">
<div className="mb-3 flex items-center justify-between">
<h4 className="font-semibold">{title}</h4>
<span className="text-sm text-muted-foreground">
{value.toFixed(2)} / 7
</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-gray-100">
<div
className="h-full rounded-full bg-emerald-500"
style={{ width: width(value) }}
/>
</div>
</div>
);
}
@@ -0,0 +1,281 @@
'use client';
import React from 'react';
import { calculateBDI2Results } from '../../test/private/BDI2Calculator';
import { useScopedI18n } from '@/locales/client';
interface BDI2ResultProps {
answers: string[];
}
export function BDI2Result({ answers }: BDI2ResultProps) {
const t = useScopedI18n('components.bdi2Result');
const tCommon = useScopedI18n('common');
// Convert answer format to the format required by calculator
const answersMap: { [key: number]: string } = {};
answers.forEach((answer, index) => {
answersMap[index + 1] = answer;
});
const results = calculateBDI2Results({
answers: answersMap,
questions: []
});
const severityNames = {
minimal: t('severity.minimal'),
mild: t('severity.mild'),
moderate: t('severity.moderate'),
severe: t('severity.severe')
};
const severityDescriptions = {
minimal: t('severityDescriptions.minimal'),
mild: t('severityDescriptions.mild'),
moderate: t('severityDescriptions.moderate'),
severe: t('severityDescriptions.severe')
};
const getSeverityColor = (severity: string) => {
switch (severity) {
case "minimal": return "text-green-600 bg-green-50 border-green-200";
case "mild": return "text-yellow-600 bg-yellow-50 border-yellow-200";
case "moderate": return "text-orange-600 bg-orange-50 border-orange-200";
case "severe": return "text-red-600 bg-red-50 border-red-200";
default: return "text-gray-600 bg-gray-50 border-gray-200";
}
};
const subscaleInfo = {
emotional: { name: t('subscales.emotional'), maxScore: 18, description: t('subscaleDescriptions.emotional') },
cognitive: { name: t('subscales.cognitive'), maxScore: 15, description: t('subscaleDescriptions.cognitive') },
somatic: { name: t('subscales.somatic'), maxScore: 15, description: t('subscaleDescriptions.somatic') },
behavioral: { name: t('subscales.behavioral'), maxScore: 15, description: t('subscaleDescriptions.behavioral') }
};
const questionTexts = [
t('symptoms.0'), t('symptoms.1'), t('symptoms.2'), t('symptoms.3'), t('symptoms.4'),
t('symptoms.5'), t('symptoms.6'), t('symptoms.7'), t('symptoms.8'), t('symptoms.9'),
t('symptoms.10'), t('symptoms.11'), t('symptoms.12'), t('symptoms.13'), t('symptoms.14'),
t('symptoms.15'), t('symptoms.16'), t('symptoms.17'), t('symptoms.18'), t('symptoms.19'),
t('symptoms.20')
];
return (
<div className="mt-6 space-y-6">
{/* Emergency warning */}
{results.suicidalIdeation && (
<div className="bg-red-100 border-2 border-red-300 rounded-lg p-6 shadow-sm">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg className="h-8 w-8 text-red-600" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-lg font-bold text-red-800">{t('labels.emergency_reminder')}</h3>
<div className="text-sm font-medium text-red-700 mt-1">
{t('crisis.suicide_warning')}
<ul className="mt-2 ml-4 space-y-1">
<li>{t('crisis.hotline')}</li>
<li>{t('crisis.hospital')}</li>
<li>{t('crisis.doctor')}</li>
<li>{t('crisis.support')}</li>
</ul>
</div>
</div>
</div>
</div>
)}
{/* Overall score */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('title')}</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<MetricCard title={tCommon('labels.total_score')} value={`${results.totalScore}/63`} />
<MetricCard title={tCommon('labels.high_score_items')} value={`${results.highScoreItemCount}/21`} />
<MetricCard
title={tCommon('labels.severity_level')}
value={severityNames[results.severity as keyof typeof severityNames] || "未知"}
className={getSeverityColor(results.severity).split(' ')[0]}
/>
</div>
</div>
{/* Severity level description */}
<div className={`border rounded-lg p-6 shadow-sm ${getSeverityColor(results.severity)}`}>
<h3 className="text-lg font-semibold mb-3">{t('labels.result_interpretation')}</h3>
<p className="text-sm mb-4">
{severityDescriptions[results.severity as keyof typeof severityDescriptions] || "评估结果异常,请重新测试。"}
</p>
<div className="space-y-2 text-sm">
<div><strong>{t('labels.scoring_criteria')}</strong></div>
<ul className="ml-4 space-y-1">
<li>{t('scoring.range_0_13')}</li>
<li>{t('scoring.range_14_19')}</li>
<li>{t('scoring.range_20_28')}</li>
<li>{t('scoring.range_29_63')}</li>
</ul>
</div>
</div>
{/* Symptom dimension analysis */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('labels.symptom_dimension_analysis')}</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Object.entries(subscaleInfo).map(([key, info]) => {
const score = results.factorScores[key] as number;
const percentage = (score / info.maxScore) * 100;
return (
<div key={key} className="border rounded-lg p-4">
<div className="flex justify-between items-center mb-2">
<span className="font-medium">{info.name}</span>
<span className="text-sm text-gray-600">{score}/{info.maxScore}</span>
</div>
<div className="mb-2">
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${percentage >= 75 ? "bg-red-400" :
percentage >= 50 ? "bg-orange-400" :
percentage >= 25 ? "bg-yellow-400" : "bg-green-400"
}`}
style={{ width: `${percentage}%` }}
></div>
</div>
</div>
<div className="text-sm text-gray-600">
{info.description}
</div>
</div>
);
})}
</div>
</div>
{/* Item analysis */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('labels.item_detailed_analysis')}</h3>
<div className="space-y-2">
{results.itemAnalysis.map((item: any, index: number) => (
<div key={item.questionId} className={`flex items-center justify-between p-3 rounded-lg ${item.questionId === 9 && item.score >= 1 ? 'bg-red-50 border border-red-200' : 'bg-gray-50'
}`}>
<div className="flex-1">
<span className="text-sm font-medium">
{index + 1}. {questionTexts[index]}
</span>
{item.questionId === 9 && item.score >= 1 && (
<div className="text-xs text-red-600 mt-1">{t('labels.suicide_risk_attention')}</div>
)}
</div>
<div className="flex items-center space-x-2">
<span className={`text-lg font-semibold ${item.questionId === 9 && item.score >= 1 ? 'text-red-700' :
item.score >= 2 ? 'text-red-600' :
item.score >= 1 ? 'text-yellow-600' : 'text-green-600'
}`}>
{item.score}
</span>
{item.isHigh && item.questionId !== 9 && (
<span className="px-2 py-1 text-xs bg-yellow-100 text-yellow-800 rounded-full">
{tCommon('labels.needs_attention')}
</span>
)}
</div>
</div>
))}
</div>
</div>
{/* Professional advice */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('labels.professional_advice')}</h3>
<div className="space-y-4 text-sm text-gray-700">
{results.severity === "minimal" ? (
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="text-green-800">
<strong>{t('advice.maintain_good_state')}</strong>
<ul className="mt-2 ml-4 space-y-1">
<li>{t('advice.maintain_good_state_item_1')}</li>
<li>{t('advice.maintain_good_state_item_2')}</li>
<li>{t('advice.maintain_good_state_item_3')}</li>
<li>{t('advice.maintain_good_state_item_4')}</li>
</ul>
</div>
</div>
) : (
<div>
<strong>{t('advice.depression_management')}</strong>
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="font-medium text-blue-900 mb-2">{t('advice.daily_management')}</h4>
<ul className="text-blue-800 space-y-1 text-sm">
<li>{t('advice.daily_management_item_1')}</li>
<li>{t('advice.daily_management_item_2')}</li>
<li>{t('advice.daily_management_item_3')}</li>
<li>{t('advice.daily_management_item_4')}</li>
<li>{t('advice.daily_management_item_5')}</li>
</ul>
</div>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
<h4 className="font-medium text-purple-900 mb-2">{t('advice.social_support')}</h4>
<ul className="text-purple-800 space-y-1 text-sm">
<li>{t('advice.social_support_item_1')}</li>
<li>{t('advice.social_support_item_2')}</li>
<li>{t('advice.social_support_item_3')}</li>
<li>{t('advice.social_support_item_4')}</li>
<li>{t('advice.social_support_item_5')}</li>
</ul>
</div>
</div>
</div>
)}
{(results.severity === "moderate" || results.severity === "severe") && (
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4">
<div className="text-orange-900">
<strong>{t('advice.professional_treatment')}</strong>
<ul className="mt-2 ml-4 space-y-1">
<li>{t('advice.professional_treatment_item_1')}</li>
<li>{t('advice.professional_treatment_item_2')}</li>
<li>{t('advice.professional_treatment_item_3')}</li>
<li>{t('advice.professional_treatment_item_4')}</li>
<li>{t('advice.professional_treatment_item_5')}</li>
</ul>
</div>
</div>
)}
<div className="bg-gray-50 border border-gray-200 rounded p-3">
<p className="text-gray-800">
<strong>{t('labels.important_reminder')}</strong>{t('disclaimer')}
</p>
</div>
</div>
</div>
</div>
);
}
interface MetricCardProps {
title: string;
value: React.ReactNode;
className?: string;
}
function MetricCard({ title, value, className = '' }: MetricCardProps) {
return (
<div className="bg-gray-50 rounded-lg p-4 flex flex-col items-center">
<span className="text-sm text-gray-500 mb-1">{title}</span>
<span className={`text-2xl font-semibold ${className || 'text-indigo-600'}`}>
{value}
</span>
</div>
);
}
@@ -0,0 +1,197 @@
'use client';
import {
calculateBigFiveResults,
calculateIpipNeoResults,
ipipNeoItemsByVersion,
} from '../../test/private/BigFiveCalculator';
import { ipipNeoFacets } from '@/questionairies/bigfive/neo-data';
interface BigFiveResultProps {
answers: string[];
version?: 50 | 120 | 300;
}
interface ScoreResult {
score: number;
average: number;
itemCount?: number;
}
const domainLabels = {
neuroticism: {
name: '神经质',
desc: '情绪敏感、压力反应以及体验焦虑、烦躁等负面情绪的倾向。',
},
extraversion: {
name: '外向性',
desc: '社交投入、自信表达、活跃度和积极情绪。',
},
openness: {
name: '开放性',
desc: '想象力、审美、求知、尝新和价值观开放程度。',
},
agreeableness: {
name: '宜人性',
desc: '信任、真诚、合作、利他、谦逊和同情倾向。',
},
conscientiousness: {
name: '尽责性',
desc: '自我效能、条理、责任、自律、成就追求和谨慎。',
},
};
const shortLabels = {
extraversion: domainLabels.extraversion,
agreeableness: domainLabels.agreeableness,
conscientiousness: domainLabels.conscientiousness,
emotionalStability: {
name: '情绪稳定性',
desc: '情绪平稳、压力耐受和较少焦虑烦躁。',
},
openness: domainLabels.openness,
};
function level(average: number) {
if (average >= 3.8) return '较高';
if (average <= 2.4) return '较低';
return '中等';
}
function barWidth(average: number) {
return `${Math.max(0, Math.min(100, ((average - 1) / 4) * 100))}%`;
}
function ScoreCard({
name,
desc,
result,
maxScore,
}: {
name: string;
desc: string;
result: ScoreResult;
maxScore: number;
}) {
return (
<div className="border rounded-lg p-5 bg-white shadow-sm">
<div className="flex items-start justify-between gap-4 mb-3">
<div>
<h4 className="font-semibold">{name}</h4>
<p className="text-sm text-muted-foreground mt-1">{desc}</p>
</div>
<div className="text-right shrink-0">
<div className="text-2xl font-semibold text-indigo-600">
{result.score}
</div>
<div className="text-xs text-muted-foreground">/ {maxScore}</div>
</div>
</div>
<div className="h-2 rounded-full bg-gray-100 overflow-hidden">
<div
className="h-full rounded-full bg-indigo-500"
style={{ width: barWidth(result.average) }}
/>
</div>
<div className="mt-3 text-sm">
<span className="font-medium">{level(result.average)}</span>
<span className="text-muted-foreground ml-2">
{result.average.toFixed(2)} / 5
</span>
</div>
</div>
);
}
export function BigFiveResult({ answers, version = 50 }: BigFiveResultProps) {
if (version === 50) {
const results = calculateBigFiveResults(answers);
return (
<div className="mt-6 space-y-6">
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-sm text-muted-foreground">
50
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Object.entries(shortLabels).map(([key, info]) => (
<ScoreCard
key={key}
name={info.name}
desc={info.desc}
result={results[key] as ScoreResult}
maxScore={50}
/>
))}
</div>
</div>
);
}
const items = ipipNeoItemsByVersion[version];
const results = calculateIpipNeoResults(answers, [...items]);
const domainItemCount = version === 120 ? 24 : 60;
const facetItemCount = version === 120 ? 4 : 10;
return (
<div className="mt-6 space-y-8">
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-2">
IPIP-NEO {version}
</h3>
<p className="text-sm text-muted-foreground">
30
</p>
</div>
<section>
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Object.entries(domainLabels).map(([key, info]) => (
<ScoreCard
key={key}
name={info.name}
desc={info.desc}
result={results.domains[key] as ScoreResult}
maxScore={domainItemCount * 5}
/>
))}
</div>
</section>
<section>
<h3 className="text-lg font-semibold mb-4">30</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{Object.entries(ipipNeoFacets).map(([key, facet]) => {
const result = results.facets[key] as ScoreResult;
return (
<div key={key} className="border rounded-lg bg-white p-4">
<div className="flex items-center justify-between gap-3">
<h4 className="font-medium">{facet.name}</h4>
<span className="text-sm font-semibold text-indigo-600">
{result.score} / {facetItemCount * 5}
</span>
</div>
<div className="mt-3 h-1.5 rounded-full bg-gray-100 overflow-hidden">
<div
className="h-full rounded-full bg-emerald-500"
style={{ width: barWidth(result.average) }}
/>
</div>
<div className="mt-2 text-xs text-muted-foreground">
{level(result.average)} · {result.average.toFixed(2)} / 5
</div>
</div>
);
})}
</div>
</section>
<div className="bg-gray-50 border rounded-lg p-4 text-sm text-gray-700">
</div>
</div>
);
}
@@ -0,0 +1,59 @@
'use client';
import { calculateCRTResults } from '../../test/private/CRTCalculator';
interface CRTResultProps {
answers: string[];
}
function summary(score: number) {
if (score >= 6) return '反思推理表现较强,能较好地抑制直觉错误。';
if (score >= 3) return '反思推理表现中等,有些题目能停下来重新检查。';
return '本次更容易受直觉答案影响,建议在关键判断中刻意放慢。';
}
export function CRTResult({ answers }: CRTResultProps) {
const result = calculateCRTResults(answers);
return (
<div className="mt-6 space-y-6">
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">CRT </h3>
<div className="flex items-end gap-3">
<span className="text-4xl font-semibold text-indigo-600">
{result.score}
</span>
<span className="text-muted-foreground mb-1">/ {result.total} </span>
</div>
<p className="text-sm text-gray-700 mt-4">{summary(result.score)}</p>
</div>
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h4 className="font-semibold mb-3"></h4>
<div className="space-y-2">
{result.items.map((item) => (
<div
key={item.questionId}
className="flex items-center justify-between rounded border p-3 text-sm"
>
<span> {item.questionId} </span>
<span
className={
item.isCorrect
? 'text-green-700 font-medium'
: 'text-red-700 font-medium'
}
>
{item.isCorrect ? '正确' : `错误,正确选项:${item.correct}`}
</span>
</div>
))}
</div>
</div>
<div className="bg-gray-50 border rounded-lg p-4 text-sm text-gray-700">
CRT
</div>
</div>
);
}
@@ -0,0 +1,76 @@
'use client';
import { calculateCareerAnchorsResults, CareerAnchor } from '../../test/private/CareerAnchorsCalculator';
interface CareerAnchorsResultProps {
answers: string[];
}
const descriptions: Record<CareerAnchor, string> = {
technical: "适合持续积累专业深度,通过专业能力获得成就和认可。",
managerial: "适合整合资源、带团队、处理复杂组织目标和责任。",
autonomy: "适合自由度较高、能自主安排方法和节奏的工作模式。",
security: "适合稳定、规则明确、风险可控且保障清晰的环境。",
entrepreneurial: "适合从零创造、开拓机会、主导项目或业务方向。",
service: "适合使命驱动、对他人或社会有明确贡献感的工作。",
challenge: "适合高难度、高竞争、需要突破限制的问题场景。",
lifestyle: "适合能与健康、家庭、自由时间和整体生活协调的职业路径。",
};
function width(score: number) {
return `${Math.max(0, Math.min(100, ((score - 1) / 4) * 100))}%`;
}
export function CareerAnchorsResult({ answers }: CareerAnchorsResultProps) {
const results = calculateCareerAnchorsResults(answers);
return (
<div className="mt-6 space-y-6">
<div className="border bg-white p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">Career Anchors </h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<MetricCard title="主要职业锚" value={results.primary.name} score={results.primary.score.toFixed(2)} />
<MetricCard title="辅助职业锚" value={results.secondary.name} score={results.secondary.score.toFixed(2)} />
</div>
</div>
<div className="border bg-blue-50 p-6 text-blue-900 shadow-sm">
<h3 className="text-lg font-semibold mb-3">
{results.primary.name} + {results.secondary.name}
</h3>
<p className="text-sm">
</p>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{results.ranked.map((item) => (
<div key={item.id} className="border bg-white p-5 shadow-sm">
<div className="mb-3 flex items-center justify-between gap-3">
<h4 className="font-semibold">{item.name}</h4>
<span className="text-sm text-muted-foreground">{item.score.toFixed(2)} / 5</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-gray-100">
<div className="h-full rounded-full bg-emerald-500" style={{ width: width(item.score) }} />
</div>
<p className="mt-3 text-sm text-gray-700">{descriptions[item.id]}</p>
</div>
))}
</div>
<div className="border bg-gray-50 p-4 text-sm text-gray-700">
</div>
</div>
);
}
function MetricCard({ title, value, score }: { title: string; value: string; score: string }) {
return (
<div className="rounded-lg bg-gray-50 p-4 text-center">
<div className="text-sm text-muted-foreground">{title}</div>
<div className="mt-1 text-2xl font-semibold text-indigo-600">{value}</div>
<div className="text-xs text-muted-foreground">{score} / 5</div>
</div>
);
}
@@ -0,0 +1,271 @@
'use client';
import React from 'react';
import { calculateDASS21Results } from '../../test/private/DASS21Calculator';
import { useScopedI18n } from '@/locales/client';
interface DASS21ResultProps {
answers: string[];
}
export function DASS21Result({ answers }: DASS21ResultProps) {
const t = useScopedI18n('components.dass21Result');
// Convert answer format to the format required by calculator
const answersMap: { [key: number]: string } = {};
answers.forEach((answer, index) => {
answersMap[index + 1] = answer;
});
const results = calculateDASS21Results({
answers: answersMap,
questions: []
});
const severityNames = {
normal: t('severity.normal'),
mild: t('severity.mild'),
moderate: t('severity.moderate'),
severe: t('severity.severe'),
extremely_severe: t('severity.extremely_severe')
};
const getSeverityColor = (severity: string) => {
switch (severity) {
case "normal": return "text-green-600 bg-green-50 border-green-200";
case "mild": return "text-yellow-600 bg-yellow-50 border-yellow-200";
case "moderate": return "text-orange-600 bg-orange-50 border-orange-200";
case "severe": return "text-red-600 bg-red-50 border-red-200";
case "extremely_severe": return "text-red-700 bg-red-100 border-red-300";
default: return "text-gray-600 bg-gray-50 border-gray-200";
}
};
const dimensionInfo = {
depression: {
name: t('dimensions.depression'),
score: results.depressionScore,
severity: results.depressionSeverity,
description: t('descriptions.depression'),
maxScore: 42
},
anxiety: {
name: t('dimensions.anxiety'),
score: results.anxietyScore,
severity: results.anxietySeverity,
description: t('descriptions.anxiety'),
maxScore: 42
},
stress: {
name: t('dimensions.stress'),
score: results.stressScore,
severity: results.stressSeverity,
description: t('descriptions.stress'),
maxScore: 42
}
};
return (
<div className="mt-6 space-y-6">
{/* Overall score */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('title')}</h3>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<MetricCard title={t('labels.total_score')} value={`${results.totalScore}/63`} />
<MetricCard title={t('labels.depression_score')} value={`${results.depressionScore}/42`} />
<MetricCard title={t('labels.anxiety_score')} value={`${results.anxietyScore}/42`} />
<MetricCard title={t('labels.stress_score')} value={`${results.stressScore}/42`} />
</div>
</div>
{/* Three-dimension analysis */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('labels.three_dimension_analysis')}</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{Object.entries(dimensionInfo).map(([key, info]) => (
<div key={key} className={`border rounded-lg p-4 ${getSeverityColor(info.severity)}`}>
<div className="text-center mb-3">
<h4 className="font-semibold text-lg">{info.name}</h4>
<div className="text-2xl font-bold mt-2">{info.score}</div>
<div className="text-sm opacity-75">/{info.maxScore}</div>
</div>
<div className="mb-3">
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${info.severity === "normal" ? "bg-green-400" :
info.severity === "mild" ? "bg-yellow-400" :
info.severity === "moderate" ? "bg-orange-400" :
info.severity === "severe" ? "bg-red-400" : "bg-red-600"
}`}
style={{ width: `${(info.score / info.maxScore) * 100}%` }}
></div>
</div>
</div>
<div className="text-center">
<span className={`inline-block px-3 py-1 rounded-full text-sm font-medium ${info.severity === "normal" ? "bg-green-100 text-green-800" :
info.severity === "mild" ? "bg-yellow-100 text-yellow-800" :
info.severity === "moderate" ? "bg-orange-100 text-orange-800" :
info.severity === "severe" ? "bg-red-100 text-red-800" : "bg-red-200 text-red-900"
}`}>
{severityNames[info.severity as keyof typeof severityNames]}
</span>
</div>
<div className="text-sm text-center mt-2 opacity-75">
{info.description}
</div>
</div>
))}
</div>
</div>
{/* Severity level standards */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('labels.scoring_criteria')}</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="space-y-2">
<h4 className="font-medium text-green-700">{t('labels.depression_dimension')}</h4>
<div className="text-sm space-y-1">
<div>{t('scoring.depression.normal')}</div>
<div>{t('scoring.depression.mild')}</div>
<div>{t('scoring.depression.moderate')}</div>
<div>{t('scoring.depression.severe')}</div>
<div>{t('scoring.depression.extremely_severe')}</div>
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium text-blue-700">{t('labels.anxiety_dimension')}</h4>
<div className="text-sm space-y-1">
<div>{t('scoring.anxiety.normal')}</div>
<div>{t('scoring.anxiety.mild')}</div>
<div>{t('scoring.anxiety.moderate')}</div>
<div>{t('scoring.anxiety.severe')}</div>
<div>{t('scoring.anxiety.extremely_severe')}</div>
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium text-purple-700">{t('labels.stress_dimension')}</h4>
<div className="text-sm space-y-1">
<div>{t('scoring.stress.normal')}</div>
<div>{t('scoring.stress.mild')}</div>
<div>{t('scoring.stress.moderate')}</div>
<div>{t('scoring.stress.severe')}</div>
<div>{t('scoring.stress.extremely_severe')}</div>
</div>
</div>
</div>
</div>
{/* Result interpretation and recommendations */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('labels.result_interpretation_advice')}</h3>
<div className="space-y-4">
{/* Overall assessment */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="font-medium text-blue-900 mb-2">{t('labels.overall_assessment')}</h4>
<div className="text-sm text-blue-800">
{results.isSevere ? (
<p>{t('assessment.severe_message')}</p>
) : (
<p>{t('assessment.normal_message')}</p>
)}
</div>
</div>
{/* Dimension-specific recommendations */}
{results.depressionSeverity !== "normal" && (
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<h4 className="font-medium text-green-900 mb-2">{t('advice.depression_dimension')}</h4>
<div className="text-sm text-green-800 space-y-1">
<p>{t('advice.depression_item_1')}</p>
<p>{t('advice.depression_item_2')}</p>
<p>{t('advice.depression_item_3')}</p>
<p>{t('advice.depression_item_4')}</p>
{(results.depressionSeverity === "severe" || results.depressionSeverity === "extremely_severe") && (
<p className="font-medium">{t('advice.depression_severe')}</p>
)}
</div>
</div>
)}
{results.anxietySeverity !== "normal" && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<h4 className="font-medium text-yellow-900 mb-2">{t('advice.anxiety_dimension')}</h4>
<div className="text-sm text-yellow-800 space-y-1">
<p>{t('advice.anxiety_item_1')}</p>
<p>{t('advice.anxiety_item_2')}</p>
<p>{t('advice.anxiety_item_3')}</p>
<p>{t('advice.anxiety_item_4')}</p>
{(results.anxietySeverity === "severe" || results.anxietySeverity === "extremely_severe") && (
<p className="font-medium">{t('advice.anxiety_severe')}</p>
)}
</div>
</div>
)}
{results.stressSeverity !== "normal" && (
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
<h4 className="font-medium text-purple-900 mb-2">{t('advice.stress_dimension')}</h4>
<div className="text-sm text-purple-800 space-y-1">
<p>{t('advice.stress_item_1')}</p>
<p>{t('advice.stress_item_2')}</p>
<p>{t('advice.stress_item_3')}</p>
<p>{t('advice.stress_item_4')}</p>
{(results.stressSeverity === "severe" || results.stressSeverity === "extremely_severe") && (
<p className="font-medium">{t('advice.stress_severe')}</p>
)}
</div>
</div>
)}
{/* Severe situation warning */}
{results.isSevere && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<div className="text-sm font-medium text-red-800">
{t('labels.important_reminder')}{t('warning.severe_distress')}
</div>
</div>
</div>
</div>
)}
<div className="bg-gray-50 border border-gray-200 rounded-lg p-3">
<p className="text-gray-800 text-sm">
<strong>{t('labels.note')}</strong>{t('disclaimer')}
</p>
</div>
</div>
</div>
</div>
);
}
interface MetricCardProps {
title: string;
value: React.ReactNode;
className?: string;
}
function MetricCard({ title, value, className = '' }: MetricCardProps) {
return (
<div className="bg-gray-50 rounded-lg p-4 flex flex-col items-center">
<span className="text-sm text-gray-500 mb-1">{title}</span>
<span className={`text-2xl font-semibold ${className || 'text-indigo-600'}`}>
{value}
</span>
</div>
);
}
@@ -0,0 +1,58 @@
'use client';
import { calculateDarkTriadResults } from '../../test/private/DarkTriadCalculator';
interface DarkTriadResultProps {
answers: string[];
}
function width(value: number) {
return `${Math.max(0, Math.min(100, ((value - 1) / 4) * 100))}%`;
}
export function DarkTriadResult({ answers }: DarkTriadResultProps) {
const results = calculateDarkTriadResults(answers);
return (
<div className="mt-6 space-y-6">
<div className="border bg-white p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<BarCard title="马基雅维利主义" value={results.machiavellianism} />
<BarCard title="自恋" value={results.narcissism} />
<BarCard title="冷酷冲动" value={results.psychopathy} />
</div>
</div>
<div className="border bg-blue-50 p-6 text-blue-900 shadow-sm">
<h3 className="text-lg font-semibold mb-3"></h3>
<p className="text-sm">
</p>
</div>
<div className="border bg-gray-50 p-4 text-sm text-gray-700">
</div>
</div>
);
}
function BarCard({ title, value }: { title: string; value: number }) {
return (
<div className="rounded-lg border bg-white p-5 shadow-sm">
<div className="mb-3 flex items-center justify-between">
<h4 className="font-semibold">{title}</h4>
<span className="text-sm text-muted-foreground">
{value.toFixed(2)} / 5
</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-gray-100">
<div
className="h-full rounded-full bg-emerald-500"
style={{ width: width(value) }}
/>
</div>
</div>
);
}
@@ -0,0 +1,59 @@
'use client';
import { calculateEmpathyResults } from '../../test/private/EmpathyCalculator';
interface EmpathyResultProps {
answers: string[];
}
function width(value: number) {
return `${Math.max(0, Math.min(100, ((value - 1) / 4) * 100))}%`;
}
export function EmpathyResult({ answers }: EmpathyResultProps) {
const results = calculateEmpathyResults(answers);
return (
<div className="mt-6 space-y-6">
<div className="border bg-white p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<BarCard title="观点采择" value={results.perspectiveTaking} />
<BarCard title="共情关怀" value={results.empathicConcern} />
<BarCard title="个人痛苦" value={results.personalDistress} />
<BarCard title="想象代入" value={results.fantasy} />
</div>
</div>
<div className="border bg-blue-50 p-6 text-blue-900 shadow-sm">
<h3 className="text-lg font-semibold mb-3"></h3>
<p className="text-sm">
怀
</p>
</div>
<div className="border bg-gray-50 p-4 text-sm text-gray-700">
IRI更适合看四个分项组合
</div>
</div>
);
}
function BarCard({ title, value }: { title: string; value: number }) {
return (
<div className="rounded-lg border bg-white p-5 shadow-sm">
<div className="mb-3 flex items-center justify-between">
<h4 className="font-semibold">{title}</h4>
<span className="text-sm text-muted-foreground">
{value.toFixed(2)} / 5
</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-gray-100">
<div
className="h-full rounded-full bg-emerald-500"
style={{ width: width(value) }}
/>
</div>
</div>
);
}
@@ -0,0 +1,79 @@
'use client';
import { calculateFisherResults, FisherDimension } from '../../test/private/FisherCalculator';
interface FisherResultProps {
answers: string[];
}
const descriptions: Record<FisherDimension, string> = {
explorer: '偏好新鲜体验、自由探索和灵活变化,适合开放、变化快、允许试错的环境。',
builder: '重视秩序、责任和稳定关系,适合目标清楚、节奏可靠、需要长期维护的环境。',
director: '偏好逻辑、效率和直接决策,适合需要分析、判断、系统设计和明确目标的环境。',
negotiator: '重视共情、意义和关系协调,适合需要理解人、连接观点和处理复杂情境的环境。',
};
const colorClass: Record<FisherDimension, string> = {
explorer: 'bg-emerald-500',
builder: 'bg-blue-500',
director: 'bg-indigo-500',
negotiator: 'bg-rose-500',
};
export function FisherResult({ answers }: FisherResultProps) {
const results = calculateFisherResults(answers);
return (
<div className="mt-6 space-y-6">
<div className="border bg-white p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">Fisher </h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<MetricCard title="主要气质" value={results.primary.name} score={`${results.primary.score}/40`} />
<MetricCard title="辅助气质" value={results.secondary.name} score={`${results.secondary.score}/40`} />
</div>
</div>
<div className="border bg-blue-50 p-6 text-blue-900 shadow-sm">
<h3 className="text-lg font-semibold mb-3">
{results.primary.name} + {results.secondary.name}
</h3>
<p className="text-sm">
{results.primary.name}{results.secondary.name}Fisher模型更适合看
</p>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{results.ranked.map((item) => (
<div key={item.id} className="border bg-white p-5 shadow-sm">
<div className="mb-3 flex items-center justify-between gap-3">
<h4 className="font-semibold">{item.name}</h4>
<span className="text-sm text-muted-foreground">{item.score}/40</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-gray-100">
<div
className={`h-full rounded-full ${colorClass[item.id]}`}
style={{ width: `${item.percentage}%` }}
/>
</div>
<p className="mt-3 text-sm text-gray-700">{descriptions[item.id]}</p>
</div>
))}
</div>
<div className="border bg-gray-50 p-4 text-sm text-gray-700">
Fisher气质量表用于人格风格参考使
</div>
</div>
);
}
function MetricCard({ title, value, score }: { title: string; value: string; score: string }) {
return (
<div className="rounded-lg bg-gray-50 p-4 text-center">
<div className="text-sm text-muted-foreground">{title}</div>
<div className="mt-1 text-2xl font-semibold text-indigo-600">{value}</div>
<div className="text-xs text-muted-foreground">{score}</div>
</div>
);
}
@@ -0,0 +1,198 @@
'use client';
import React from 'react';
import { calculateGAD7Results } from '../../test/private/GAD7Calculator';
import { useScopedI18n } from '@/locales/client';
interface GAD7ResultProps {
answers: string[];
}
export function GAD7Result({ answers }: GAD7ResultProps) {
const t = useScopedI18n('components.gad7Result');
// Convert answer format to the format required by calculator
const answersMap: { [key: number]: string } = {};
answers.forEach((answer, index) => {
answersMap[index + 1] = answer;
});
const results = calculateGAD7Results({
answers: answersMap,
questions: []
});
const severityNames = {
minimal: t('severity.minimal'),
mild: t('severity.mild'),
moderate: t('severity.moderate'),
severe: t('severity.severe')
};
const severityDescriptions = {
minimal: t('severityDescriptions.minimal'),
mild: t('severityDescriptions.mild'),
moderate: t('severityDescriptions.moderate'),
severe: t('severityDescriptions.severe')
};
const getSeverityColor = (severity: string) => {
switch (severity) {
case "minimal": return "text-green-600 bg-green-50 border-green-200";
case "mild": return "text-yellow-600 bg-yellow-50 border-yellow-200";
case "moderate": return "text-orange-600 bg-orange-50 border-orange-200";
case "severe": return "text-red-600 bg-red-50 border-red-200";
default: return "text-gray-600 bg-gray-50 border-gray-200";
}
};
const questionTexts = [
t('questions.0'), t('questions.1'), t('questions.2'), t('questions.3'),
t('questions.4'), t('questions.5'), t('questions.6')
];
return (
<div className="mt-6 space-y-6">
{/* Overall score */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('title')}</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<MetricCard title={t('labels.total_score')} value={`${results.totalScore}/21`} />
<MetricCard title={t('labels.high_score_items')} value={`${results.highScoreItemCount}/7`} />
<MetricCard
title={t('labels.anxiety_level')}
value={severityNames[results.severity as keyof typeof severityNames] || t('labels.unknown')}
className={getSeverityColor(results.severity).split(' ')[0]}
/>
</div>
</div>
{/* Severity level description */}
<div className={`border rounded-lg p-6 shadow-sm ${getSeverityColor(results.severity)}`}>
<h3 className="text-lg font-semibold mb-3">{t('labels.result_interpretation')}</h3>
<p className="text-sm mb-4">
{severityDescriptions[results.severity as keyof typeof severityDescriptions] || "评估结果异常,请重新测试。"}
</p>
<div className="space-y-2 text-sm">
<div><strong>{t('labels.scoring_criteria')}</strong></div>
<ul className="ml-4 space-y-1">
<li>{t('scoring.range_0_4')}</li>
<li>{t('scoring.range_5_9')}</li>
<li>{t('scoring.range_10_14')}</li>
<li>{t('scoring.range_15_21')}</li>
</ul>
</div>
</div>
{/* Item analysis */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('labels.item_analysis')}</h3>
<div className="space-y-3">
{results.itemAnalysis.map((item: any, index: number) => (
<div key={item.questionId} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex-1">
<span className="text-sm font-medium">
{index + 1}. {questionTexts[index]}
</span>
</div>
<div className="flex items-center space-x-2">
<span className={`text-lg font-semibold ${item.score >= 2 ? 'text-red-600' :
item.score >= 1 ? 'text-yellow-600' : 'text-green-600'
}`}>
{item.score}
</span>
{item.isHigh && (
<span className="px-2 py-1 text-xs bg-red-100 text-red-800 rounded-full">
{t('labels.needs_attention')}
</span>
)}
</div>
</div>
))}
</div>
{results.highScoreItemCount > 0 && (
<div className="mt-4 bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<h4 className="font-medium text-yellow-900 mb-2">{t('labels.high_score_item_alert')}</h4>
<div className="text-sm text-yellow-800">
{t('highScoreAlert.message', { count: results.highScoreItemCount })}
</div>
</div>
)}
</div>
{/* Professional advice */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('labels.professional_advice')}</h3>
<div className="space-y-3 text-sm text-gray-700">
{results.severity === "minimal" ? (
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="text-green-800">
<strong>{t('advice.maintain_good_state')}</strong>
<ul className="mt-2 ml-4 space-y-1">
<li>{t('advice.maintain_good_state_item_1')}</li>
<li>{t('advice.maintain_good_state_item_2')}</li>
<li>{t('advice.maintain_good_state_item_3')}</li>
<li>{t('advice.maintain_good_state_item_4')}</li>
</ul>
</div>
</div>
) : (
<div>
<strong>{t('advice.self_management_advice')}</strong>
<ul className="mt-2 ml-4 space-y-1">
<li>{t('advice.self_management_item_1')}</li>
<li>{t('advice.self_management_item_2')}</li>
<li>{t('advice.self_management_item_3')}</li>
<li>{t('advice.self_management_item_4')}</li>
<li>{t('advice.self_management_item_5')}</li>
</ul>
</div>
)}
{(results.severity === "moderate" || results.severity === "severe") && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<div className="text-sm font-medium text-red-800">
{t('advice.professional_help_message', { severity: severityNames[results.severity as keyof typeof severityNames] })}
</div>
</div>
</div>
</div>
)}
<div className="bg-blue-50 border border-blue-200 rounded p-3">
<p className="text-blue-800">
<strong>{t('labels.note')}</strong>{t('disclaimer')}
</p>
</div>
</div>
</div>
</div>
);
}
interface MetricCardProps {
title: string;
value: React.ReactNode;
className?: string;
}
function MetricCard({ title, value, className = '' }: MetricCardProps) {
return (
<div className="bg-gray-50 rounded-lg p-4 flex flex-col items-center">
<span className="text-sm text-gray-500 mb-1">{title}</span>
<span className={`text-2xl font-semibold ${className || 'text-indigo-600'}`}>
{value}
</span>
</div>
);
}
@@ -0,0 +1,149 @@
'use client';
import React from 'react';
import { useScopedI18n } from '@/locales/client';
import { calculateGDResults } from '../../test/private/GDCalculator';
function useLabels() {
const t = useScopedI18n('components.gdResult');
return {
totalScore: t('totalScore'),
scorePercentage: t('scorePercentage'),
elevatedItems: t('elevatedItems'),
genderIdentity: t('genderIdentity'),
socialRole: t('socialRole'),
physicalDysphoria: t('physicalDysphoria'),
genderExpression: t('genderExpression'),
overallAssessment: t('overallAssessment'),
recommendations: t('recommendations'),
importantNotes: t('importantNotes'),
understandingResults: t('understandingResults'),
factorScores: t('factorScores'),
interpretationLevels: {
low: t('interpretationLevels.low'),
mild: t('interpretationLevels.mild'),
moderate: t('interpretationLevels.moderate'),
high: t('interpretationLevels.high'),
},
factorDescriptions: {
genderIdentity: t('factorDescriptions.genderIdentity'),
socialRole: t('factorDescriptions.socialRole'),
physicalDysphoria: t('factorDescriptions.physicalDysphoria'),
genderExpression: t('factorDescriptions.genderExpression'),
},
notes: {
purpose: t('notes.purpose'),
substitute: t('notes.substitute'),
complexity: t('notes.complexity'),
professional: t('notes.professional'),
},
recommendationTexts: {
high: t('recommendationTexts.high'),
low: t('recommendationTexts.low'),
},
};
}
export function GDResult({
answers,
}: {
answers: string[];
}) {
const labels = useLabels();
// Convert answers array to object format expected by calculator
const answersObj: { [key: number]: string } = {};
answers.forEach((answer, index) => {
answersObj[index + 1] = answer;
});
const results = calculateGDResults({ answers: answersObj, questions: [] });
const getInterpretationColor = (interpretation: string) => {
switch (interpretation) {
case 'low': return 'text-green-600';
case 'mild': return 'text-yellow-600';
case 'moderate': return 'text-orange-600';
case 'high': return 'text-red-600';
default: return 'text-gray-600';
}
};
const getInterpretationLabel = (interpretation: string) => {
return labels.interpretationLevels[interpretation as keyof typeof labels.interpretationLevels] || 'Unknown';
};
return (
<div className="mt-6 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<MetricCard title={labels.totalScore} value={results.totalScore} />
<MetricCard title={labels.scorePercentage} value={`${results.scorePercentage}%`} />
<MetricCard title={labels.elevatedItems} value={results.positiveItemCount} />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<MetricCard title={labels.genderIdentity} value={results.factorScores.genderIdentity} />
<MetricCard title={labels.socialRole} value={results.factorScores.socialRole} />
<MetricCard title={labels.physicalDysphoria} value={results.factorScores.physicalDysphoria} />
<MetricCard title={labels.genderExpression} value={results.factorScores.genderExpression} />
</div>
<div className="bg-white border rounded-lg p-4 shadow-sm">
<h3 className="text-sm font-medium text-gray-500 mb-2">{labels.overallAssessment}</h3>
<div className={`text-lg font-semibold ${getInterpretationColor(results.interpretation)}`}>
{getInterpretationLabel(results.interpretation)} Level
</div>
<p className="text-sm text-gray-600 mt-1">
Score: {results.totalScore}/189 ({results.scorePercentage}%)
</p>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 className="text-sm font-medium text-blue-800 mb-2">{labels.recommendations}</h3>
<p className="text-sm text-blue-700">
{results.scorePercentage >= 50 ? labels.recommendationTexts.high : labels.recommendationTexts.low}
</p>
</div>
<div className="bg-gray-50 border rounded-lg p-4">
<h3 className="text-sm font-medium text-gray-700 mb-2">{labels.importantNotes}</h3>
<ul className="text-sm text-gray-600 space-y-1">
<li> {labels.notes.purpose}</li>
<li> {labels.notes.substitute}</li>
<li> {labels.notes.complexity}</li>
<li> {labels.notes.professional}</li>
</ul>
</div>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
<h3 className="text-sm font-medium text-purple-800 mb-2">{labels.understandingResults}</h3>
<div className="text-sm text-purple-700 space-y-2">
<p><strong>{labels.factorScores}:</strong></p>
<ul className="ml-4 space-y-1">
<li> {labels.genderIdentity}: {labels.factorDescriptions.genderIdentity}</li>
<li> {labels.socialRole}: {labels.factorDescriptions.socialRole}</li>
<li> {labels.physicalDysphoria}: {labels.factorDescriptions.physicalDysphoria}</li>
<li> {labels.genderExpression}: {labels.factorDescriptions.genderExpression}</li>
</ul>
</div>
</div>
</div>
);
}
interface MetricCardProps {
title: string;
value: React.ReactNode;
className?: string;
}
function MetricCard({ title, value, className = '' }: MetricCardProps) {
return (
<div
className={`bg-white border rounded-lg p-4 flex flex-col items-center shadow-sm ${className}`}
>
<span className="text-sm text-gray-500 mb-1">{title}</span>
<span className="text-2xl font-semibold text-indigo-600">{value}</span>
</div>
);
}
@@ -0,0 +1,96 @@
'use client';
import { calculateGritResults } from '../../test/private/GritCalculator';
interface GritResultProps {
answers: string[];
}
const levelText = {
low: '坚毅程度偏低',
moderate: '坚毅程度中等',
high: '坚毅程度较高',
};
const levelDescription = {
low: '你当前在长期目标上的持续投入或兴趣稳定性可能偏弱。更适合从目标拆小、减少干扰、明确反馈开始,而不是单靠意志力硬撑。',
moderate: '你具备一定的坚持能力,但在目标很长期、反馈很慢或兴趣变化较快时,可能会出现波动。',
high: '你通常能围绕长期目标持续努力,也较不容易被短期挫折带偏。继续保留调整目标的弹性会更稳。',
};
function width(value: number) {
return `${Math.max(0, Math.min(100, ((value - 1) / 4) * 100))}%`;
}
export function GritResult({ answers }: GritResultProps) {
const results = calculateGritResults(answers);
return (
<div className="mt-6 space-y-6">
<div className="border bg-white p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<MetricCard title="总平均分" value={results.totalAverage.toFixed(2)} />
<MetricCard
title="努力坚持"
value={results.perseveranceAverage.toFixed(2)}
/>
<MetricCard
title="兴趣稳定"
value={results.consistencyAverage.toFixed(2)}
/>
</div>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<BarCard title="努力坚持" value={results.perseveranceAverage} />
<BarCard title="兴趣稳定" value={results.consistencyAverage} />
</div>
<div className="border bg-blue-50 p-6 text-blue-900 shadow-sm">
<h3 className="text-lg font-semibold mb-3">{levelText[results.level]}</h3>
<p className="text-sm">{levelDescription[results.level]}</p>
</div>
<div className="border bg-gray-50 p-4 text-sm text-gray-700">
Grit
</div>
</div>
);
}
interface MetricCardProps {
title: string;
value: string;
}
function MetricCard({ title, value }: MetricCardProps) {
return (
<div className="rounded-lg bg-gray-50 p-4 text-center">
<div className="text-sm text-muted-foreground">{title}</div>
<div className="mt-1 text-2xl font-semibold text-indigo-600">
{value}
</div>
<div className="text-xs text-muted-foreground">/ 5</div>
</div>
);
}
function BarCard({ title, value }: { title: string; value: number }) {
return (
<div className="rounded-lg border bg-white p-5 shadow-sm">
<div className="mb-3 flex items-center justify-between">
<h4 className="font-semibold">{title}</h4>
<span className="text-sm text-muted-foreground">
{value.toFixed(2)} / 5
</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-gray-100">
<div
className="h-full rounded-full bg-emerald-500"
style={{ width: width(value) }}
/>
</div>
</div>
);
}
@@ -0,0 +1,67 @@
'use client';
import { calculateHEXACOResults } from '../../test/private/HEXACOCalculator';
interface HEXACOResultProps {
answers: string[];
}
const labels = [
['诚实谦逊', 'honestyHumility'],
['情绪性', 'emotionality'],
['外向性', 'extraversion'],
['宜人性', 'agreeableness'],
['尽责性', 'conscientiousness'],
['开放性', 'openness'],
] as const;
function width(value: number) {
return `${Math.max(0, Math.min(100, ((value - 1) / 4) * 100))}%`;
}
export function HEXACOResult({ answers }: HEXACOResultProps) {
const results = calculateHEXACOResults(answers);
return (
<div className="mt-6 space-y-6">
<div className="border bg-white p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">HEXACO </h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{labels.map(([label, key]) => (
<BarCard key={key} title={label} value={results[key]} />
))}
</div>
</div>
<div className="border bg-blue-50 p-6 text-blue-900 shadow-sm">
<h3 className="text-lg font-semibold mb-3"></h3>
<p className="text-sm">
HEXACO Big Five
</p>
</div>
<div className="border bg-gray-50 p-4 text-sm text-gray-700">
Big FiveRIASEC
</div>
</div>
);
}
function BarCard({ title, value }: { title: string; value: number }) {
return (
<div className="rounded-lg border bg-white p-5 shadow-sm">
<div className="mb-3 flex items-center justify-between">
<h4 className="font-semibold">{title}</h4>
<span className="text-sm text-muted-foreground">
{value.toFixed(2)} / 5
</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-gray-100">
<div
className="h-full rounded-full bg-emerald-500"
style={{ width: width(value) }}
/>
</div>
</div>
);
}
@@ -0,0 +1,95 @@
'use client';
import React from 'react';
import { calculateISIResults } from '../../test/private/ISICalculator';
import { useScopedI18n } from '@/locales/client';
interface ISIResultProps {
answers: string[];
}
export function ISIResult({ answers }: ISIResultProps) {
const t = useScopedI18n('components.isiResult');
const answersMap: { [key: number]: string } = {};
answers.forEach((answer, index) => {
answersMap[index + 1] = answer;
});
const results = calculateISIResults({
answers: answersMap,
questions: []
});
const severityNames = {
no_insomnia: t('severity.no_insomnia'),
subthreshold: t('severity.subthreshold'),
moderate: t('severity.moderate'),
severe: t('severity.severe')
};
const getSeverityColor = (severity: string) => {
switch (severity) {
case "no_insomnia": return "text-green-600 bg-green-50 border-green-200";
case "subthreshold": return "text-yellow-600 bg-yellow-50 border-yellow-200";
case "moderate": return "text-orange-600 bg-orange-50 border-orange-200";
case "severe": return "text-red-600 bg-red-50 border-red-200";
default: return "text-gray-600 bg-gray-50 border-gray-200";
}
};
return (
<div className="mt-6 space-y-6">
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('title')}</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<MetricCard title={t('labels.total_score')} value={`${results.totalScore}/28`} />
<MetricCard title={t('labels.high_score_items')} value={`${results.highScoreItemCount}/7`} />
<MetricCard
title={t('labels.insomnia_level')}
value={severityNames[results.severity as keyof typeof severityNames] || t('labels.unknown')}
className={getSeverityColor(results.severity).split(' ')[0]}
/>
</div>
</div>
<div className={`border rounded-lg p-6 shadow-sm ${getSeverityColor(results.severity)}`}>
<h3 className="text-lg font-semibold mb-3">{t('labels.result_interpretation')}</h3>
<div className="space-y-2 text-sm">
<div><strong>{t('labels.scoring_criteria')}</strong></div>
<ul className="ml-4 space-y-1">
<li>{t('scoring.range_0_7')}</li>
<li>{t('scoring.range_8_14')}</li>
<li>{t('scoring.range_15_21')}</li>
<li>{t('scoring.range_22_28')}</li>
</ul>
</div>
</div>
{results.isSevere && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="text-sm font-medium text-red-800">
{t('advice.sleep_specialist_message')}
</div>
</div>
)}
</div>
);
}
interface MetricCardProps {
title: string;
value: React.ReactNode;
className?: string;
}
function MetricCard({ title, value, className = '' }: MetricCardProps) {
return (
<div className="bg-gray-50 rounded-lg p-4 flex flex-col items-center">
<span className="text-sm text-gray-500 mb-1">{title}</span>
<span className={`text-2xl font-semibold ${className || 'text-indigo-600'}`}>
{value}
</span>
</div>
);
}
@@ -0,0 +1,83 @@
'use client';
import { calculateMaximizerResults } from '../../test/private/MaximizerCalculator';
interface MaximizerResultProps {
answers: string[];
}
const levelText = {
satisficer: '满意型选择风格',
balanced: '平衡型选择风格',
maximizer: '最大化选择风格',
};
const levelDescription = {
satisficer: '你更倾向于在达到标准后做决定,选择成本较低,也更容易保持行动效率。',
balanced: '你会在重要选择中认真比较,但通常不会让比较无限扩大。这个区间相对灵活。',
maximizer: '你更倾向于追求最优选择,会投入较多比较和信息搜集。优势是标准高,风险是决策成本和后悔感增加。',
};
function width(value: number) {
return `${Math.max(0, Math.min(100, ((value - 1) / 6) * 100))}%`;
}
export function MaximizerResult({ answers }: MaximizerResultProps) {
const results = calculateMaximizerResults(answers);
return (
<div className="mt-6 space-y-6">
<div className="border bg-white p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<MetricCard title="平均分" value={`${results.average.toFixed(2)}/7`} />
<MetricCard title="选择风格" value={levelText[results.level]} />
<MetricCard title="题目数" value="13题" />
</div>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<BarCard title="高标准" value={results.highStandards} />
<BarCard title="选项搜索" value={results.search} />
<BarCard title="决策困难" value={results.difficulty} />
</div>
<div className="border bg-blue-50 p-6 text-blue-900 shadow-sm">
<h3 className="text-lg font-semibold mb-3"></h3>
<p className="text-sm">{levelDescription[results.level]}</p>
</div>
<div className="border bg-gray-50 p-4 text-sm text-gray-700">
</div>
</div>
);
}
function MetricCard({ title, value }: { title: string; value: string }) {
return (
<div className="rounded-lg bg-gray-50 p-4 text-center">
<div className="text-sm text-muted-foreground">{title}</div>
<div className="mt-1 text-2xl font-semibold text-indigo-600">{value}</div>
</div>
);
}
function BarCard({ title, value }: { title: string; value: number }) {
return (
<div className="rounded-lg border bg-white p-5 shadow-sm">
<div className="mb-3 flex items-center justify-between">
<h4 className="font-semibold">{title}</h4>
<span className="text-sm text-muted-foreground">
{value.toFixed(2)} / 7
</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-gray-100">
<div
className="h-full rounded-full bg-emerald-500"
style={{ width: width(value) }}
/>
</div>
</div>
);
}
@@ -0,0 +1,181 @@
'use client';
import React from 'react';
import { useScopedI18n } from '@/locales/client';
import { calculateNPDResults } from '../../test/private/NPDCalculator';
function useLabels() {
const t = useScopedI18n('components.npdResult');
return {
totalScore: t('totalScore'),
percentile: t('percentile'),
leadership: t('leadership'),
exhibitionism: t('exhibitionism'),
narcissisticTraitsLevel: t('narcissisticTraitsLevel'),
dominantTrait: t('dominantTrait'),
entitlement: t('entitlement'),
interpretation: t('interpretation'),
understandingTraits: t('understandingTraits'),
factorBreakdown: t('factorBreakdown'),
importantNotes: t('importantNotes'),
healthyVsProblematic: t('healthyVsProblematic'),
interpretationLevels: {
low: t('interpretationLevels.low'),
average: t('interpretationLevels.average'),
above_average: t('interpretationLevels.above_average'),
high: t('interpretationLevels.high'),
},
traitLabels: {
leadership: t('traitLabels.leadership'),
exhibitionism: t('traitLabels.exhibitionism'),
entitlement: t('traitLabels.entitlement'),
},
factorDescriptions: {
leadership: t('factorDescriptions.leadership'),
exhibitionism: t('factorDescriptions.exhibitionism'),
entitlement: t('factorDescriptions.entitlement'),
},
notes: {
continuum: t('notes.continuum'),
adaptive: t('notes.adaptive'),
disorder: t('notes.disorder'),
purpose: t('notes.purpose'),
population: t('notes.population'),
},
healthyAspects: t('healthyAspects'),
potentialConcerns: t('potentialConcerns'),
balanceKey: t('balanceKey'),
recommendationTexts: {
high: t('recommendationTexts.high'),
above_average: t('recommendationTexts.above_average'),
low: t('recommendationTexts.low'),
},
};
}
export function NPDResult({
answers,
}: {
answers: string[];
}) {
const labels = useLabels();
// Convert answers array to object format expected by calculator
const answersObj: { [key: number]: string } = {};
answers.forEach((answer, index) => {
answersObj[index + 1] = answer;
});
const results = calculateNPDResults({ answers: answersObj, questions: [] });
const getInterpretationColor = (interpretation: string) => {
switch (interpretation) {
case 'low': return 'text-green-600';
case 'average': return 'text-blue-600';
case 'above_average': return 'text-yellow-600';
case 'high': return 'text-red-600';
default: return 'text-gray-600';
}
};
const getInterpretationLabel = (interpretation: string) => {
return labels.interpretationLevels[interpretation as keyof typeof labels.interpretationLevels] || 'Unknown';
};
const getDominantTraitLabel = (trait: string) => {
return labels.traitLabels[trait as keyof typeof labels.traitLabels] || 'Unknown';
};
return (
<div className="mt-6 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<MetricCard title={labels.totalScore} value={`${results.totalScore}/16`} />
<MetricCard title={labels.percentile} value={`${results.percentile}th`} />
<MetricCard title={labels.leadership} value={results.factorScores.leadership} />
<MetricCard title={labels.exhibitionism} value={results.factorScores.exhibitionism} />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white border rounded-lg p-4 shadow-sm">
<h3 className="text-sm font-medium text-gray-500 mb-2">{labels.narcissisticTraitsLevel}</h3>
<div className={`text-lg font-semibold ${getInterpretationColor(results.interpretation)}`}>
{getInterpretationLabel(results.interpretation)}
</div>
<p className="text-sm text-gray-600 mt-1">
Score: {results.totalScore}/16 ({results.percentile}th percentile)
</p>
</div>
<div className="bg-white border rounded-lg p-4 shadow-sm">
<h3 className="text-sm font-medium text-gray-500 mb-2">{labels.dominantTrait}</h3>
<div className="text-lg font-semibold text-purple-600">
{getDominantTraitLabel(results.dominantTrait)}
</div>
<p className="text-sm text-gray-600 mt-1">
{labels.entitlement}: {results.factorScores.entitlement}
</p>
</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 className="text-sm font-medium text-blue-800 mb-2">{labels.interpretation}</h3>
<p className="text-sm text-blue-700">
{results.interpretation === "high"
? labels.recommendationTexts.high
: results.interpretation === "above_average"
? labels.recommendationTexts.above_average
: labels.recommendationTexts.low}
</p>
</div>
<div className="bg-gray-50 border rounded-lg p-4">
<h3 className="text-sm font-medium text-gray-700 mb-2">{labels.understandingTraits}</h3>
<div className="text-sm text-gray-600 space-y-2">
<p><strong>{labels.factorBreakdown}:</strong></p>
<ul className="ml-4 space-y-1">
<li> <strong>{labels.traitLabels.leadership} ({results.factorScores.leadership}):</strong> {labels.factorDescriptions.leadership}</li>
<li> <strong>{labels.traitLabels.exhibitionism} ({results.factorScores.exhibitionism}):</strong> {labels.factorDescriptions.exhibitionism}</li>
<li> <strong>{labels.traitLabels.entitlement} ({results.factorScores.entitlement}):</strong> {labels.factorDescriptions.entitlement}</li>
</ul>
</div>
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<h3 className="text-sm font-medium text-yellow-800 mb-2">{labels.importantNotes}</h3>
<ul className="text-sm text-yellow-700 space-y-1">
<li> {labels.notes.continuum}</li>
<li> {labels.notes.adaptive}</li>
<li> {labels.notes.disorder}</li>
<li> {labels.notes.purpose}</li>
<li> {labels.notes.population}</li>
</ul>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<h3 className="text-sm font-medium text-green-800 mb-2">{labels.healthyVsProblematic}</h3>
<div className="text-sm text-green-700 space-y-2">
<p><strong>{labels.healthyAspects}</strong></p>
<p><strong>{labels.potentialConcerns}</strong></p>
<p>{labels.balanceKey}</p>
</div>
</div>
</div>
);
}
interface MetricCardProps {
title: string;
value: React.ReactNode;
className?: string;
}
function MetricCard({ title, value, className = '' }: MetricCardProps) {
return (
<div
className={`bg-white border rounded-lg p-4 flex flex-col items-center shadow-sm ${className}`}
>
<span className="text-sm text-gray-500 mb-1">{title}</span>
<span className="text-2xl font-semibold text-indigo-600">{value}</span>
</div>
);
}
@@ -0,0 +1,82 @@
'use client';
import { calculateNeedForCognitionResults } from '../../test/private/NeedForCognitionCalculator';
interface NeedForCognitionResultProps {
answers: string[];
}
const levelText = {
low: '认知需求偏低',
moderate: '认知需求中等',
high: '认知需求较高',
};
const levelDescription = {
low: '你可能更偏好清晰、直接、低认知负荷的处理方式。面对复杂问题时,降低进入门槛和明确收益会更有帮助。',
moderate: '你会根据情境投入思考:重要、有兴趣或有价值的问题更容易激发你深入加工。',
high: '你通常喜欢深入思考、分析复杂问题和寻找新解法。注意在低风险问题上保留效率感,会更平衡。',
};
function width(value: number) {
return `${Math.max(0, Math.min(100, ((value - 1) / 4) * 100))}%`;
}
export function NeedForCognitionResult({ answers }: NeedForCognitionResultProps) {
const results = calculateNeedForCognitionResults(answers);
return (
<div className="mt-6 space-y-6">
<div className="border bg-white p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<MetricCard title="总分" value={`${results.total}/90`} />
<MetricCard title="平均分" value={`${results.average.toFixed(2)}/5`} />
<MetricCard title="结果水平" value={levelText[results.level]} />
</div>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<BarCard title="思考投入" value={results.thinkingAverage} />
<BarCard title="智力挑战偏好" value={results.challengeAverage} />
</div>
<div className="border bg-blue-50 p-6 text-blue-900 shadow-sm">
<h3 className="text-lg font-semibold mb-3"></h3>
<p className="text-sm">{levelDescription[results.level]}</p>
</div>
<div className="border bg-gray-50 p-4 text-sm text-gray-700">
CRT结果结合理解
</div>
</div>
);
}
function MetricCard({ title, value }: { title: string; value: string }) {
return (
<div className="rounded-lg bg-gray-50 p-4 text-center">
<div className="text-sm text-muted-foreground">{title}</div>
<div className="mt-1 text-2xl font-semibold text-indigo-600">{value}</div>
</div>
);
}
function BarCard({ title, value }: { title: string; value: number }) {
return (
<div className="rounded-lg border bg-white p-5 shadow-sm">
<div className="mb-3 flex items-center justify-between">
<h4 className="font-semibold">{title}</h4>
<span className="text-sm text-muted-foreground">
{value.toFixed(2)} / 5
</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-gray-100">
<div
className="h-full rounded-full bg-emerald-500"
style={{ width: width(value) }}
/>
</div>
</div>
);
}
@@ -0,0 +1,77 @@
'use client';
import React from 'react';
import { useScopedI18n } from '@/locales/client';
function useLabels() {
const t = useScopedI18n('components.ocdResult');
return {
total: t('totalScore'),
obs: t('obsessionsScore'),
comp: t('compulsionsScore'),
severity: t('severity'),
severityMap: {
1: t('severityLevel.1'),
2: t('severityLevel.2'),
3: t('severityLevel.3'),
4: t('severityLevel.4'),
5: t('severityLevel.5'),
} as Record<number, string>,
};
}
export function OCDResult({
answers,
}: {
answers: string[];
}) {
const labels = useLabels();
const toNumber = (v: string | undefined) => Number(v) || 0;
let obsessionsScore = 0;
let compulsionsScore = 0;
for (let i = 1; i <= 10; i++) {
const score = toNumber(answers[i - 1]); // array index start 0
if (i <= 5) obsessionsScore += score;
else compulsionsScore += score;
}
const totalScore = obsessionsScore + compulsionsScore;
let severityLevel = 0;
if (totalScore >= 32) severityLevel = 5;
else if (totalScore >= 24) severityLevel = 4;
else if (totalScore >= 16) severityLevel = 3;
else if (totalScore >= 8) severityLevel = 2;
else severityLevel = 1;
return (
<div className="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4">
<MetricCard title={labels.total} value={totalScore} />
<MetricCard title={labels.obs} value={obsessionsScore} />
<MetricCard title={labels.comp} value={compulsionsScore} />
<MetricCard
title={labels.severity}
value={`${labels.severityMap[severityLevel] || '?'} (${severityLevel})`}
className="md:col-span-3"
/>
</div>
);
}
interface MetricCardProps {
title: string;
value: React.ReactNode;
className?: string;
}
function MetricCard({ title, value, className = '' }: MetricCardProps) {
return (
<div
className={`bg-white border rounded-lg p-4 flex flex-col items-center shadow-sm ${className}`}
>
<span className="text-sm text-gray-500 mb-1">{title}</span>
<span className="text-2xl font-semibold text-indigo-600">{value}</span>
</div>
);
}
@@ -0,0 +1,66 @@
'use client';
import { calculateOEPSResults } from '../../test/private/OEPSCalculator';
interface OEPSResultProps {
answers: string[];
}
function barWidth(value: number) {
return `${Math.max(0, Math.min(100, (value / 5) * 100))}%`;
}
export function OEPSResult({ answers }: OEPSResultProps) {
const result = calculateOEPSResults(answers);
return (
<div className="mt-6 space-y-6">
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-2"></h3>
<p className="text-sm text-muted-foreground">
</p>
<div className="mt-4 rounded-lg border bg-indigo-50 border-indigo-200 p-4">
<div className="text-2xl font-semibold text-indigo-700">
{result.top.name}
</div>
<p className="text-sm text-indigo-900 mt-2">{result.top.description}</p>
<p className="text-sm text-indigo-900 mt-2">
{result.top.average.toFixed(2)} / 5
</p>
</div>
</div>
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h4 className="font-semibold mb-4"></h4>
<div className="space-y-3">
{result.ranked.map((item) => (
<div key={item.type} className="rounded border p-4">
<div className="flex items-start justify-between gap-4 mb-2">
<div>
<div className="font-medium">{item.name}</div>
<div className="text-sm text-muted-foreground">
{item.description}
</div>
</div>
<div className="text-right font-semibold text-indigo-600">
{item.average.toFixed(2)}
</div>
</div>
<div className="h-2 rounded-full bg-gray-100 overflow-hidden">
<div
className="h-full rounded-full bg-indigo-500"
style={{ width: barWidth(item.average) }}
/>
</div>
</div>
))}
</div>
</div>
<div className="bg-gray-50 border rounded-lg p-4 text-sm text-gray-700">
OEPS
</div>
</div>
);
}
@@ -0,0 +1,240 @@
'use client';
import React from 'react';
import { calculatePHQ9Results } from '../../test/private/PHQ9Calculator';
import { useScopedI18n } from '@/locales/client';
interface PHQ9ResultProps {
answers: string[];
}
export function PHQ9Result({ answers }: PHQ9ResultProps) {
const t = useScopedI18n('components.phq9Result');
// Convert answer format to the format required by calculator
const answersMap: { [key: number]: string } = {};
answers.forEach((answer, index) => {
answersMap[index + 1] = answer;
});
const results = calculatePHQ9Results({
answers: answersMap,
questions: []
});
const severityNames = {
minimal: t('severity.minimal'),
mild: t('severity.mild'),
moderate: t('severity.moderate'),
moderately_severe: t('severity.moderately_severe'),
severe: t('severity.severe')
};
const severityDescriptions = {
minimal: t('severityDescriptions.minimal'),
mild: t('severityDescriptions.mild'),
moderate: t('severityDescriptions.moderate'),
moderately_severe: t('severityDescriptions.moderately_severe'),
severe: t('severityDescriptions.severe')
};
const getSeverityColor = (severity: string) => {
switch (severity) {
case "minimal": return "text-green-600 bg-green-50 border-green-200";
case "mild": return "text-yellow-600 bg-yellow-50 border-yellow-200";
case "moderate": return "text-orange-600 bg-orange-50 border-orange-200";
case "moderately_severe": return "text-red-600 bg-red-50 border-red-200";
case "severe": return "text-red-700 bg-red-100 border-red-300";
default: return "text-gray-600 bg-gray-50 border-gray-200";
}
};
const questionTexts = [
t('questions.0'), t('questions.1'), t('questions.2'), t('questions.3'), t('questions.4'),
t('questions.5'), t('questions.6'), t('questions.7'), t('questions.8')
];
return (
<div className="mt-6 space-y-6">
{/* Overall score */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('title')}</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<MetricCard title={t('labels.total_score')} value={`${results.totalScore}/27`} />
<MetricCard title={t('labels.high_score_items')} value={`${results.highScoreItemCount}/9`} />
<MetricCard
title={t('labels.depression_level')}
value={severityNames[results.severity as keyof typeof severityNames] || t('labels.unknown')}
className={getSeverityColor(results.severity).split(' ')[0]}
/>
</div>
</div>
{/* Important warning */}
{results.suicidalIdeation && (
<div className="bg-red-100 border-2 border-red-300 rounded-lg p-6 shadow-sm">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg className="h-8 w-8 text-red-600" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-lg font-bold text-red-800">{t('labels.emergency_reminder')}</h3>
<div className="text-sm font-medium text-red-700 mt-1">
{t('crisis.suicide_warning')}
<ul className="mt-2 ml-4 space-y-1">
<li>{t('crisis.hotline')}</li>
<li>{t('crisis.hospital')}</li>
<li>{t('crisis.doctor')}</li>
<li>{t('crisis.support')}</li>
</ul>
</div>
</div>
</div>
</div>
)}
{/* Severity level description */}
<div className={`border rounded-lg p-6 shadow-sm ${getSeverityColor(results.severity)}`}>
<h3 className="text-lg font-semibold mb-3">{t('labels.result_interpretation')}</h3>
<p className="text-sm mb-4">
{severityDescriptions[results.severity as keyof typeof severityDescriptions] || "评估结果异常,请重新测试。"}
</p>
<div className="space-y-2 text-sm">
<div><strong>{t('labels.scoring_criteria')}</strong></div>
<ul className="ml-4 space-y-1">
<li>{t('scoring.range_0_4')}</li>
<li>{t('scoring.range_5_9')}</li>
<li>{t('scoring.range_10_14')}</li>
<li>{t('scoring.range_15_19')}</li>
<li>{t('scoring.range_20_27')}</li>
</ul>
</div>
{results.majorDepressionCriteria && (
<div className="mt-4 bg-yellow-100 border border-yellow-300 rounded p-3">
<div className="text-yellow-900 font-medium">
{t('clinical.major_depression_warning')}
</div>
</div>
)}
</div>
{/* Item analysis */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('labels.item_analysis')}</h3>
<div className="space-y-3">
{results.itemAnalysis.map((item: any, index: number) => (
<div key={item.questionId} className={`flex items-center justify-between p-3 rounded-lg ${
item.questionId === 9 && item.score >= 1 ? 'bg-red-50 border border-red-200' : 'bg-gray-50'
}`}>
<div className="flex-1">
<span className="text-sm font-medium">
{index + 1}. {questionTexts[index]}
</span>
{item.questionId === 9 && item.score >= 1 && (
<div className="text-xs text-red-600 mt-1">{t('labels.needs_immediate_attention')}</div>
)}
</div>
<div className="flex items-center space-x-2">
<span className={`text-lg font-semibold ${
item.questionId === 9 && item.score >= 1 ? 'text-red-700' :
item.score >= 2 ? 'text-red-600' :
item.score >= 1 ? 'text-yellow-600' : 'text-green-600'
}`}>
{item.score}
</span>
{item.isHigh && item.questionId !== 9 && (
<span className="px-2 py-1 text-xs bg-yellow-100 text-yellow-800 rounded-full">
{t('labels.needs_attention')}
</span>
)}
</div>
</div>
))}
</div>
{results.highScoreItemCount > 0 && (
<div className="mt-4 bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="font-medium text-blue-900 mb-2">{t('labels.high_score_item_analysis')}</h4>
<div className="text-sm text-blue-800">
{t('highScoreAnalysis.message', { count: results.highScoreItemCount })}
</div>
</div>
)}
</div>
{/* Professional advice */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('labels.professional_advice')}</h3>
<div className="space-y-3 text-sm text-gray-700">
{results.severity === "minimal" ? (
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="text-green-800">
<strong>{t('advice.maintain_good_state')}</strong>
<ul className="mt-2 ml-4 space-y-1">
<li>{t('advice.maintain_good_state_item_1')}</li>
<li>{t('advice.maintain_good_state_item_2')}</li>
<li>{t('advice.maintain_good_state_item_3')}</li>
<li>{t('advice.maintain_good_state_item_4')}</li>
</ul>
</div>
</div>
) : (
<div>
<strong>{t('advice.self_management_advice')}</strong>
<ul className="mt-2 ml-4 space-y-1">
<li>{t('advice.self_management_item_1')}</li>
<li>{t('advice.self_management_item_2')}</li>
<li>{t('advice.self_management_item_3')}</li>
<li>{t('advice.self_management_item_4')}</li>
<li>{t('advice.self_management_item_5')}</li>
<li>{t('advice.self_management_item_6')}</li>
</ul>
</div>
)}
{(results.severity === "moderate" || results.severity === "moderately_severe" || results.severity === "severe") && (
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4">
<div className="text-orange-900">
<strong>{t('advice.professional_treatment')}</strong>
<ul className="mt-2 ml-4 space-y-1">
<li>{t('advice.professional_treatment_item_1')}</li>
<li>{t('advice.professional_treatment_item_2')}</li>
<li>{t('advice.professional_treatment_item_3')}</li>
<li>{t('advice.professional_treatment_item_4')}</li>
</ul>
</div>
</div>
)}
<div className="bg-blue-50 border border-blue-200 rounded p-3">
<p className="text-blue-800">
<strong>{t('labels.note')}</strong>{t('disclaimer')}
</p>
</div>
</div>
</div>
</div>
);
}
interface MetricCardProps {
title: string;
value: React.ReactNode;
className?: string;
}
function MetricCard({ title, value, className = '' }: MetricCardProps) {
return (
<div className="bg-gray-50 rounded-lg p-4 flex flex-col items-center">
<span className="text-sm text-gray-500 mb-1">{title}</span>
<span className={`text-2xl font-semibold ${className || 'text-indigo-600'}`}>
{value}
</span>
</div>
);
}
@@ -0,0 +1,284 @@
'use client';
import React from 'react';
import { calculatePSS10Results } from '../../test/private/PSS10Calculator';
import { useScopedI18n } from '@/locales/client';
interface PSS10ResultProps {
answers: string[];
}
export function PSS10Result({ answers }: PSS10ResultProps) {
const t = useScopedI18n('components.pss10Result');
// Convert answer format to the format required by calculator
const answersMap: { [key: number]: string } = {};
answers.forEach((answer, index) => {
answersMap[index + 1] = answer;
});
const results = calculatePSS10Results({
answers: answersMap,
questions: []
});
const severityDescriptions = {
low: t('severityDescriptions.low'),
moderate: t('severityDescriptions.moderate'),
high: t('severityDescriptions.high')
};
const getSeverityColor = (severity: string) => {
switch (severity) {
case "low": return "text-green-600 bg-green-50 border-green-200";
case "moderate": return "text-yellow-600 bg-yellow-50 border-yellow-200";
case "high": return "text-red-600 bg-red-50 border-red-200";
default: return "text-gray-600 bg-gray-50 border-gray-200";
}
};
const questionTexts = [
t('questions.0'), t('questions.1'), t('questions.2'), t('questions.3'), t('questions.4'),
t('questions.5'), t('questions.6'), t('questions.7'), t('questions.8'), t('questions.9')
];
const getScoreInterpretation = (score: number) => {
if (score <= 13) return { level: t('scoreInterpretation.low_level'), color: "text-green-600", desc: t('scoreInterpretation.low_desc') };
if (score <= 26) return { level: t('scoreInterpretation.moderate_level'), color: "text-yellow-600", desc: t('scoreInterpretation.moderate_desc') };
return { level: t('scoreInterpretation.high_level'), color: "text-red-600", desc: t('scoreInterpretation.high_desc') };
};
const scoreInterp = getScoreInterpretation(results.totalScore);
return (
<div className="mt-6 space-y-6">
{/* Overall score */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('title')}</h3>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<MetricCard title={t('labels.total_score')} value={`${results.totalScore}/40`} />
<MetricCard title={t('labels.stress_perception')} value={`${results.stressPerceptionScore}/24`} />
<MetricCard title={t('labels.coping_ability')} value={`${results.copingAbilityScore}/16`} />
<MetricCard
title={t('labels.stress_level')}
value={scoreInterp.level}
className={scoreInterp.color}
/>
</div>
</div>
{/* Severity level description */}
<div className={`border rounded-lg p-6 shadow-sm ${getSeverityColor(results.severity)}`}>
<h3 className="text-lg font-semibold mb-3">{t('labels.result_interpretation')}</h3>
<p className="text-sm mb-4">
{severityDescriptions[results.severity as keyof typeof severityDescriptions]}
</p>
<div className="space-y-3 text-sm">
<div>
<strong>{t('labels.score_interpretation')}</strong>
<ul className="mt-1 ml-4 space-y-1">
<li> {t('scoring.total_range')}</li>
<li> {t('scoring.stress_perception_desc')}</li>
<li> {t('scoring.coping_ability_desc')}</li>
</ul>
</div>
<div>
<strong>{t('labels.reference_standards')}</strong>
<ul className="mt-1 ml-4 space-y-1">
<li> {t('scoring.range_0_13')}</li>
<li> {t('scoring.range_14_26')}</li>
<li> {t('scoring.range_27_40')}</li>
</ul>
</div>
</div>
</div>
{/* Subscale analysis */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('labels.subscale_analysis')}</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Stress perception */}
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="font-medium mb-3 flex items-center">
<span className="w-3 h-3 bg-red-400 rounded-full mr-2"></span>
{t('subscales.stress_perception_title')} ({results.stressPerceptionScore}/24)
</h4>
<div className="space-y-2 text-sm">
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-red-400 h-2 rounded-full"
style={{ width: `${(results.stressPerceptionScore / 24) * 100}%` }}
></div>
</div>
<p className="text-gray-700">
{t('subscales.stress_perception_desc')}
</p>
</div>
</div>
{/* Coping ability */}
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="font-medium mb-3 flex items-center">
<span className="w-3 h-3 bg-blue-400 rounded-full mr-2"></span>
{t('subscales.coping_ability_title')} ({results.copingAbilityScore}/16)
</h4>
<div className="space-y-2 text-sm">
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-400 h-2 rounded-full"
style={{ width: `${(results.copingAbilityScore / 16) * 100}%` }}
></div>
</div>
<p className="text-gray-700">
{t('subscales.coping_ability_desc')}
</p>
</div>
</div>
</div>
</div>
{/* Item analysis */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('labels.item_analysis')}</h3>
<div className="space-y-3">
{results.itemAnalysis.map((item: any, index: number) => (
<div key={item.questionId} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div className="flex-1">
<div className="flex items-center">
<span className="text-sm font-medium">
{index + 1}. {questionTexts[index]}
</span>
{item.isReverse && (
<span className="ml-2 px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full">
{t('labels.reverse_scoring')}
</span>
)}
</div>
<div className="text-xs text-gray-500 mt-1">
{t('labels.original_score')}: {item.originalScore} {item.isReverse ? `${t('labels.actual_score')}: ${item.actualScore}` : ''}
</div>
</div>
<div className="flex items-center space-x-2">
<span className={`text-lg font-semibold ${
item.actualScore >= 3 ? 'text-red-600' :
item.actualScore >= 2 ? 'text-yellow-600' : 'text-green-600'
}`}>
{item.actualScore}
</span>
{item.isHigh && (
<span className="px-2 py-1 text-xs bg-red-100 text-red-800 rounded-full">
{t('labels.high_stress')}
</span>
)}
</div>
</div>
))}
</div>
{results.highScoreItemCount > 0 && (
<div className="mt-4 bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<h4 className="font-medium text-yellow-900 mb-2">{t('labels.high_score_reminder')}</h4>
<div className="text-sm text-yellow-800">
{t('highScoreAnalysis.message', { count: results.highScoreItemCount })}
</div>
</div>
)}
</div>
{/* Stress management recommendations */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('labels.stress_management_advice')}</h3>
<div className="space-y-4 text-sm text-gray-700">
{results.severity === "low" ? (
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="text-green-800">
<strong>{t('advice.maintain_good_state')}</strong>
<ul className="mt-2 ml-4 space-y-1">
<li>{t('advice.maintain_good_state_item_1')}</li>
<li>{t('advice.maintain_good_state_item_2')}</li>
<li>{t('advice.maintain_good_state_item_3')}</li>
<li>{t('advice.maintain_good_state_item_4')}</li>
</ul>
</div>
</div>
) : (
<div>
<strong>{t('advice.stress_management_strategies')}</strong>
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Short-term strategies */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="font-medium text-blue-900 mb-2">{t('advice.short_term_strategies')}</h4>
<ul className="text-blue-800 space-y-1 text-sm">
<li>{t('advice.short_term_item_1')}</li>
<li>{t('advice.short_term_item_2')}</li>
<li>{t('advice.short_term_item_3')}</li>
<li>{t('advice.short_term_item_4')}</li>
<li>{t('advice.short_term_item_5')}</li>
</ul>
</div>
{/* Long-term strategies */}
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
<h4 className="font-medium text-purple-900 mb-2">{t('advice.long_term_strategies')}</h4>
<ul className="text-purple-800 space-y-1 text-sm">
<li>{t('advice.long_term_item_1')}</li>
<li>{t('advice.long_term_item_2')}</li>
<li>{t('advice.long_term_item_3')}</li>
<li>{t('advice.long_term_item_4')}</li>
<li>{t('advice.long_term_item_5')}</li>
</ul>
</div>
</div>
</div>
)}
{results.severity === "high" && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<div className="text-sm font-medium text-red-800">
{t('advice.high_stress_warning')}
</div>
</div>
</div>
</div>
)}
<div className="bg-gray-50 border border-gray-200 rounded p-3">
<p className="text-gray-800">
<strong>{t('labels.note')}</strong>{t('disclaimer')}
</p>
</div>
</div>
</div>
</div>
);
}
interface MetricCardProps {
title: string;
value: React.ReactNode;
className?: string;
}
function MetricCard({ title, value, className = '' }: MetricCardProps) {
return (
<div className="bg-gray-50 rounded-lg p-4 flex flex-col items-center">
<span className="text-sm text-gray-500 mb-1">{title}</span>
<span className={`text-2xl font-semibold ${className || 'text-indigo-600'}`}>
{value}
</span>
</div>
);
}
@@ -0,0 +1,79 @@
'use client';
import {
calculateRIASECResults,
riasecTypes,
} from '../../test/private/RIASECCalculator';
interface RIASECResultProps {
answers: string[];
}
const descriptions = {
R: '动手、设备、机械、户外与具体操作',
I: '研究、分析、科学与复杂问题',
A: '创作、设计、表达与开放式任务',
S: '帮助、教学、照护与合作',
E: '领导、说服、销售与组织资源',
C: '数据、记录、流程与明确规范',
};
function barWidth(average: number) {
return `${Math.max(0, Math.min(100, ((average - 1) / 4) * 100))}%`;
}
export function RIASECResult({ answers }: RIASECResultProps) {
const { scores, ranking, hollandCode } = calculateRIASECResults(answers);
return (
<div className="mt-6 space-y-6">
<div className="border bg-white p-6 shadow-sm">
<div className="text-sm text-muted-foreground"> Holland Code</div>
<div className="mt-1 text-4xl font-semibold text-indigo-600">
{hollandCode}
</div>
<p className="mt-3 text-sm text-muted-foreground">
</p>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{ranking.map(([code]) => {
const result = scores[code] as { score: number; average: number };
const type = riasecTypes[code as keyof typeof riasecTypes];
return (
<div key={code} className="rounded-lg border bg-white p-5 shadow-sm">
<div className="flex items-start justify-between gap-4">
<div>
<h4 className="font-semibold">
{code} · {type.name}
</h4>
<p className="mt-1 text-sm text-muted-foreground">
{descriptions[code as keyof typeof descriptions]}
</p>
</div>
<div className="shrink-0 text-right">
<div className="text-xl font-semibold">{result.score}</div>
<div className="text-xs text-muted-foreground">/ 40</div>
</div>
</div>
<div className="mt-4 h-2 overflow-hidden rounded-full bg-gray-100">
<div
className="h-full rounded-full bg-emerald-500"
style={{ width: barWidth(result.average) }}
/>
</div>
<div className="mt-2 text-xs text-muted-foreground">
{result.average.toFixed(2)} / 5
</div>
</div>
);
})}
</div>
<div className="border bg-gray-50 p-4 text-sm text-gray-700">
</div>
</div>
);
}
@@ -0,0 +1,119 @@
"use client";
import React from 'react';
import { OCDResult } from './OCDResult';
import { SCL90Result } from './SCL90Result';
import { SDSResult } from './SDSResult';
import { GAD7Result } from './GAD7Result';
import { PHQ9Result } from './PHQ9Result';
import { PSS10Result } from './PSS10Result';
import { DASS21Result } from './DASS21Result';
import { BDI2Result } from './BDI2Result';
import { ISIResult } from './ISIResult';
import { ADHDResult } from './ADHDResult';
import { GDResult } from './GDResult';
import { NPDResult } from './NPDResult';
import { BigFiveResult } from './BigFiveResult';
import { CRTResult } from './CRTResult';
import { OEPSResult } from './OEPSResult';
import { RIASECResult } from './RIASECResult';
import { WHO5Result } from './WHO5Result';
import { SelfEsteemResult } from './SelfEsteemResult';
import { GritResult } from './GritResult';
import { SelfControlResult } from './SelfControlResult';
import { NeedForCognitionResult } from './NeedForCognitionResult';
import { MaximizerResult } from './MaximizerResult';
import { AttachmentResult } from './AttachmentResult';
import { EmpathyResult } from './EmpathyResult';
import { DarkTriadResult } from './DarkTriadResult';
import { HEXACOResult } from './HEXACOResult';
import { FisherResult } from './FisherResult';
import { SchwartzResult } from './SchwartzResult';
import { VIAResult } from './VIAResult';
import { CareerAnchorsResult } from './CareerAnchorsResult';
interface Props {
questionnaireId: string;
answers: string[];
}
export function ResultAnalysis({ questionnaireId, answers }: Props) {
switch (questionnaireId) {
case 'bigfive':
return <BigFiveResult answers={answers} version={50} />;
case 'bigfive-120':
return <BigFiveResult answers={answers} version={120} />;
case 'bigfive-300':
return <BigFiveResult answers={answers} version={300} />;
case 'oeps':
return <OEPSResult answers={answers} />;
case 'crt':
return <CRTResult answers={answers} />;
case 'riasec':
return <RIASECResult answers={answers} />;
case 'self-esteem':
return <SelfEsteemResult answers={answers} />;
case 'grit':
return <GritResult answers={answers} />;
case 'self-control':
return <SelfControlResult answers={answers} />;
case 'need-for-cognition':
return <NeedForCognitionResult answers={answers} />;
case 'maximizer':
return <MaximizerResult answers={answers} />;
case 'attachment':
return <AttachmentResult answers={answers} />;
case 'empathy':
return <EmpathyResult answers={answers} />;
case 'dark-triad':
return <DarkTriadResult answers={answers} />;
case 'hexaco':
return <HEXACOResult answers={answers} />;
case 'fisher':
return <FisherResult answers={answers} />;
case 'schwartz':
return <SchwartzResult answers={answers} />;
case 'via':
return <VIAResult answers={answers} />;
case 'career-anchors':
return <CareerAnchorsResult answers={answers} />;
case 'ocd':
return <OCDResult answers={answers} />;
case 'scl90':
return <SCL90Result answers={answers} />;
case 'sds':
return <SDSResult answers={answers} />;
case 'gad7':
return <GAD7Result answers={answers} />;
case 'phq9':
return <PHQ9Result answers={answers} />;
case 'pss10':
return <PSS10Result answers={answers} />;
case 'dass21':
return <DASS21Result answers={answers} />;
case 'who5':
return <WHO5Result answers={answers} />;
case 'bdi2':
return <BDI2Result answers={answers} />;
case 'isi':
return <ISIResult answers={answers} />;
case 'adhd':
return <ADHDResult answers={answers} />;
case 'gd':
return <GDResult answers={answers} />;
case 'npd':
return <NPDResult answers={answers} />;
default:
return (
<div className="mt-6 p-6 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="text-yellow-800">
<h3 className="font-semibold mb-2"></h3>
<p className="text-sm">
ID为 &quot;{questionnaireId}&quot;
</p>
</div>
</div>
);
}
}
@@ -0,0 +1,156 @@
'use client';
import React from 'react';
import { useScopedI18n } from '@/locales/client';
import { calculateSCL90Results } from '../../test/private/SCL90Calculator';
interface SCL90ResultProps {
answers: string[];
}
export function SCL90Result({ answers }: SCL90ResultProps) {
const t = useScopedI18n('components.scl90Result');
const tCommon = useScopedI18n('common');
// Convert answer format to the format required by calculator
const answersMap: { [key: number]: string } = {};
answers.forEach((answer, index) => {
answersMap[index + 1] = answer;
});
const results = calculateSCL90Results({
answers: answersMap,
questions: []
});
const factorNames = {
somatization: t('factors.somatization'),
obsessive: t('factors.obsessive'),
interpersonal: t('factors.interpersonal'),
depression: t('factors.depression'),
anxiety: t('factors.anxiety'),
hostility: t('factors.hostility'),
phobic: t('factors.phobic'),
paranoid: t('factors.paranoid'),
psychotic: t('factors.psychotic'),
other: t('factors.other')
};
const severityNames = {
normal: tCommon('severity.normal'),
mild: tCommon('severity.mild'),
moderate: tCommon('severity.moderate'),
severe: tCommon('severity.severe')
};
const getSeverityColor = (severity: string) => {
switch (severity) {
case "normal": return "text-green-600";
case "mild": return "text-yellow-600";
case "moderate": return "text-orange-600";
case "severe": return "text-red-600";
default: return "text-gray-600";
}
};
const getFactorSeverity = (score: number) => {
if (score >= 3) return "severe";
if (score >= 2) return "moderate";
if (score >= 1.5) return "mild";
return "normal";
};
return (
<div className="mt-6 space-y-6">
{/* Overall score */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('labels.overall_assessment')}</h3>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<MetricCard title={tCommon('labels.total_score')} value={results.totalScore} />
<MetricCard title={t('labels.positive_item_count')} value={results.positiveItemCount} />
<MetricCard title={t('labels.positive_symptom_average')} value={results.positiveItemAverage.toFixed(2)} />
<MetricCard
title={tCommon('labels.severity_level')}
value={severityNames[results.severity as keyof typeof severityNames] || t('warnings.unknown_level')}
className={getSeverityColor(results.severity)}
/>
</div>
</div>
{/* Factor scores */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('labels.factor_analysis')}</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(results.factorScores).map(([factor, score]) => {
const factorSeverity = getFactorSeverity(Number(score));
return (
<div key={factor} className="border rounded-lg p-4">
<div className="flex justify-between items-center mb-2">
<span className="font-medium">{factorNames[factor as keyof typeof factorNames]}</span>
<span className={`text-sm px-2 py-1 rounded ${getSeverityColor(factorSeverity)}`}>
{severityNames[factorSeverity as keyof typeof severityNames]}
</span>
</div>
<div className="text-2xl font-bold text-indigo-600">{Number(score).toFixed(2)}</div>
</div>
);
})}
</div>
</div>
{/* Result interpretation */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{tCommon('labels.result_interpretation')}</h3>
<div className="space-y-3 text-sm text-gray-700">
<div>
<strong>{t('clinical.rating_criteria')}</strong>
<ul className="mt-1 ml-4 space-y-1">
<li> {t('clinical.rating_scale')}</li>
</ul>
</div>
<div>
<strong>{t('clinical.judgment_criteria')}</strong>
<ul className="mt-1 ml-4 space-y-1">
<li> {t('clinical.total_score_criteria')}</li>
<li> {t('clinical.factor_score_2')}</li>
<li> {t('clinical.factor_score_3')}</li>
</ul>
</div>
{results.isSevere && (
<div className="bg-red-50 border border-red-200 rounded p-3 mt-4">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<div className="text-sm font-medium text-red-800">
{t('warnings.severe_condition')}
</div>
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
}
interface MetricCardProps {
title: string;
value: React.ReactNode;
className?: string;
}
function MetricCard({ title, value, className = '' }: MetricCardProps) {
return (
<div className="bg-gray-50 rounded-lg p-4 flex flex-col items-center">
<span className="text-sm text-gray-500 mb-1">{title}</span>
<span className={`text-2xl font-semibold ${className || 'text-indigo-600'}`}>
{value}
</span>
</div>
);
}
@@ -0,0 +1,171 @@
'use client';
import React from 'react';
import { calculateSDSResults } from '../../test/private/SDSCalculator';
import { useScopedI18n } from '@/locales/client';
interface SDSResultProps {
answers: string[];
}
export function SDSResult({ answers }: SDSResultProps) {
const t = useScopedI18n('components.sdsResult');
// Convert answer format to the format required by calculator
const answersMap: { [key: number]: string } = {};
answers.forEach((answer, index) => {
answersMap[index + 1] = answer;
});
const results = calculateSDSResults({
answers: answersMap,
questions: []
});
const severityNames = {
normal: t('severity.normal'),
mild: t('severity.mild'),
moderate: t('severity.moderate'),
severe: t('severity.severe')
};
const severityDescriptions = {
normal: t('severityDescriptions.normal'),
mild: t('severityDescriptions.mild'),
moderate: t('severityDescriptions.moderate'),
severe: t('severityDescriptions.severe')
};
const getSeverityColor = (severity: string) => {
switch (severity) {
case "normal": return "text-green-600 bg-green-50 border-green-200";
case "mild": return "text-yellow-600 bg-yellow-50 border-yellow-200";
case "moderate": return "text-orange-600 bg-orange-50 border-orange-200";
case "severe": return "text-red-600 bg-red-50 border-red-200";
default: return "text-gray-600 bg-gray-50 border-gray-200";
}
};
// Get raw score (not multiplied by 1.25)
const rawScore = Math.round(results.totalScore / 1.25);
return (
<div className="mt-6 space-y-6">
{/* Overall score */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('title')}</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<MetricCard title={t('labels.raw_total_score')} value={`${rawScore}/80`} />
<MetricCard title={t('labels.standard_score')} value={results.totalScore} />
<MetricCard
title={t('labels.depression_level')}
value={severityNames[results.severity as keyof typeof severityNames] || t('labels.unknown')}
className={getSeverityColor(results.severity).split(' ')[0]}
/>
</div>
</div>
{/* Severity level description */}
<div className={`border rounded-lg p-6 shadow-sm ${getSeverityColor(results.severity)}`}>
<h3 className="text-lg font-semibold mb-3">{t('labels.result_interpretation')}</h3>
<p className="text-sm mb-4">
{severityDescriptions[results.severity as keyof typeof severityDescriptions]}
</p>
<div className="space-y-2 text-sm">
<div><strong>{t('labels.scoring_criteria')}</strong></div>
<ul className="ml-4 space-y-1">
<li> {t('scoring.range_0_52')}</li>
<li> {t('scoring.range_53_62')}</li>
<li> {t('scoring.range_63_72')}</li>
<li> {t('scoring.range_73_plus')}</li>
</ul>
</div>
</div>
{/* Item analysis */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('labels.detailed_analysis')}</h3>
<div className="space-y-4">
{/* High score items reminder */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="font-medium text-blue-900 mb-2">{t('labels.scale_description')}</h4>
<div className="text-sm text-blue-800 space-y-1">
<p> {t('scaleInfo.description_1')}</p>
<p> {t('scaleInfo.description_2')}</p>
<p> {t('scaleInfo.description_3')}</p>
</div>
</div>
{/* Scoring method description */}
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
<h4 className="font-medium text-gray-900 mb-2">{t('labels.scoring_method')}</h4>
<div className="text-sm text-gray-700 space-y-1">
<p><strong></strong>{t('scaleInfo.positive_items')}</p>
<p><strong></strong>{t('scaleInfo.reverse_items')}</p>
<p><strong></strong>{t('scaleInfo.option_scoring')}</p>
<p><strong></strong>{t('scaleInfo.reverse_scoring')}</p>
</div>
</div>
{(results.severity === "severe" || results.severity === "moderate") && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<div className="text-sm font-medium text-red-800">
{t('labels.important_reminder')}{t('warnings.depression_reminder')}
</div>
</div>
</div>
</div>
)}
</div>
</div>
{/* Professional advice */}
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">{t('labels.professional_advice')}</h3>
<div className="space-y-3 text-sm text-gray-700">
<div>
<strong>{t('advice.high_score_title')}</strong>
<ul className="mt-2 ml-4 space-y-1">
<li> {t('advice.seek_professional')}</li>
<li> {t('advice.share_feelings')}</li>
<li> {t('advice.maintain_routine')}</li>
<li> {t('advice.avoid_substances')}</li>
<li> {t('advice.suicide_help')}</li>
</ul>
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded p-3">
<p className="text-yellow-800">
<strong>{t('labels.note')}</strong>{t('disclaimer')}
</p>
</div>
</div>
</div>
</div>
);
}
interface MetricCardProps {
title: string;
value: React.ReactNode;
className?: string;
}
function MetricCard({ title, value, className = '' }: MetricCardProps) {
return (
<div className="bg-gray-50 rounded-lg p-4 flex flex-col items-center">
<span className="text-sm text-gray-500 mb-1">{title}</span>
<span className={`text-2xl font-semibold ${className || 'text-indigo-600'}`}>
{value}
</span>
</div>
);
}
@@ -0,0 +1,133 @@
'use client';
import {
calculateSchwartzResults,
SchwartzHigherOrder,
SchwartzValue,
} from '../../test/private/SchwartzCalculator';
interface SchwartzResultProps {
answers: string[];
}
const valueDescriptions: Record<SchwartzValue, string> = {
selfDirection: '重视独立思考、自由选择和创造。',
stimulation: '重视新奇、挑战、变化和兴奋体验。',
hedonism: '重视愉悦、享受和积极体验。',
achievement: '重视能力表现、努力成果和外部认可。',
power: '重视影响力、资源、地位和掌控感。',
security: '重视稳定、安全、健康和生活保障。',
conformity: '重视规则、克制和对他人影响的边界。',
tradition: '重视文化、家庭、仪式和既有传承。',
benevolence: '重视照顾亲近的人、忠诚和可靠支持。',
universalism: '重视公平、包容、自然和更广泛的人群福祉。',
};
const higherOrderDescriptions: Record<SchwartzHigherOrder, string> = {
opennessToChange: '偏向自由、探索、变化和个人选择。',
conservation: '偏向秩序、稳定、规范和连续性。',
selfEnhancement: '偏向成就、影响力和个人资源增长。',
selfTranscendence: '偏向关怀、公平、包容和超越个人利益。',
};
function width(score: number) {
return `${Math.max(0, Math.min(100, ((score - 1) / 4) * 100))}%`;
}
export function SchwartzResult({ answers }: SchwartzResultProps) {
const results = calculateSchwartzResults(answers);
const topValues = results.rankedValues.slice(0, 3);
return (
<div className="mt-6 space-y-6">
<div className="border bg-white p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">Schwartz </h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
{topValues.map((item, index) => (
<MetricCard
key={item.id}
title={`${index + 1} 优先价值`}
value={item.name}
score={item.score.toFixed(2)}
/>
))}
</div>
</div>
<div className="border bg-blue-50 p-6 text-blue-900 shadow-sm">
<h3 className="text-lg font-semibold mb-3">
{topValues.map((item) => item.name).join(' / ')}
</h3>
<p className="text-sm">
</p>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{results.rankedHigherOrders.map((item) => (
<ScoreCard
key={item.id}
title={item.name}
score={item.score}
description={higherOrderDescriptions[item.id]}
color="bg-indigo-500"
/>
))}
</div>
<div className="border bg-white p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{results.rankedValues.map((item) => (
<ScoreCard
key={item.id}
title={item.name}
score={item.score}
description={valueDescriptions[item.id]}
color="bg-emerald-500"
/>
))}
</div>
</div>
<div className="border bg-gray-50 p-4 text-sm text-gray-700">
</div>
</div>
);
}
function MetricCard({ title, value, score }: { title: string; value: string; score: string }) {
return (
<div className="rounded-lg bg-gray-50 p-4 text-center">
<div className="text-sm text-muted-foreground">{title}</div>
<div className="mt-1 text-2xl font-semibold text-indigo-600">{value}</div>
<div className="text-xs text-muted-foreground">{score} / 5</div>
</div>
);
}
function ScoreCard({
title,
score,
description,
color,
}: {
title: string;
score: number;
description: string;
color: string;
}) {
return (
<div className="border bg-white p-5 shadow-sm">
<div className="mb-3 flex items-center justify-between gap-3">
<h4 className="font-semibold">{title}</h4>
<span className="text-sm text-muted-foreground">{score.toFixed(2)} / 5</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-gray-100">
<div className={`h-full rounded-full ${color}`} style={{ width: width(score) }} />
</div>
<p className="mt-3 text-sm text-gray-700">{description}</p>
</div>
);
}
@@ -0,0 +1,89 @@
'use client';
import { calculateSelfControlResults } from '../../test/private/SelfControlCalculator';
interface SelfControlResultProps {
answers: string[];
}
const levelText = {
low: '自控力偏低',
moderate: '自控力中等',
high: '自控力较高',
};
const levelDescription = {
low: '你可能比较容易受到诱惑、情绪或即时满足影响。比起责备自己,更有效的方向通常是减少诱因、降低开始成本,并把计划拆得更具体。',
moderate: '你具备一定的自控能力,但在压力、疲劳或诱惑较强的场景中可能会波动。环境设计和习惯系统会很有帮助。',
high: '你通常能较好地管理冲动、抵抗诱惑并推进计划。继续注意休息和目标弹性,能避免把自律变成过度紧绷。',
};
function width(value: number) {
return `${Math.max(0, Math.min(100, ((value - 1) / 4) * 100))}%`;
}
export function SelfControlResult({ answers }: SelfControlResultProps) {
const results = calculateSelfControlResults(answers);
return (
<div className="mt-6 space-y-6">
<div className="border bg-white p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<MetricCard title="总分" value={`${results.total}/65`} />
<MetricCard title="平均分" value={`${results.average.toFixed(2)}/5`} />
<MetricCard title="结果水平" value={levelText[results.level]} />
</div>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<BarCard title="冲动管理" value={results.impulseAverage} />
<BarCard title="计划执行" value={results.executionAverage} />
</div>
<div className="border bg-blue-50 p-6 text-blue-900 shadow-sm">
<h3 className="text-lg font-semibold mb-3"></h3>
<p className="text-sm">{levelDescription[results.level]}</p>
</div>
<div className="border bg-gray-50 p-4 text-sm text-gray-700">
</div>
</div>
);
}
interface MetricCardProps {
title: string;
value: string;
}
function MetricCard({ title, value }: MetricCardProps) {
return (
<div className="rounded-lg bg-gray-50 p-4 text-center">
<div className="text-sm text-muted-foreground">{title}</div>
<div className="mt-1 text-2xl font-semibold text-indigo-600">
{value}
</div>
</div>
);
}
function BarCard({ title, value }: { title: string; value: number }) {
return (
<div className="rounded-lg border bg-white p-5 shadow-sm">
<div className="mb-3 flex items-center justify-between">
<h4 className="font-semibold">{title}</h4>
<span className="text-sm text-muted-foreground">
{value.toFixed(2)} / 5
</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-gray-100">
<div
className="h-full rounded-full bg-emerald-500"
style={{ width: width(value) }}
/>
</div>
</div>
);
}
@@ -0,0 +1,77 @@
'use client';
import { calculateSelfEsteemResults } from '../../test/private/SelfEsteemCalculator';
interface SelfEsteemResultProps {
answers: string[];
}
const levelText = {
low: '自尊偏低',
moderate: '自尊中等',
high: '自尊较高',
};
const levelDescription = {
low: '你当前的整体自我评价可能偏严苛,容易在挫折、人际评价或压力情境中否定自己。这个结果适合提醒你关注自我接纳、情绪状态和支持系统。',
moderate: '你的整体自尊处在较常见区间,通常能够看到自身价值,但在压力或失败体验下可能仍会有明显波动。',
high: '你对自身价值和能力通常有较稳定、积极的评价。继续保持现实、温和且有弹性的自我认识会更有帮助。',
};
function barWidth(score: number) {
return `${Math.max(0, Math.min(100, (score / 30) * 100))}%`;
}
export function SelfEsteemResult({ answers }: SelfEsteemResultProps) {
const results = calculateSelfEsteemResults(answers);
const levelClass =
results.level === 'high'
? 'border-green-200 bg-green-50 text-green-800'
: results.level === 'moderate'
? 'border-blue-200 bg-blue-50 text-blue-800'
: 'border-yellow-200 bg-yellow-50 text-yellow-800';
return (
<div className="mt-6 space-y-6">
<div className="border bg-white p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
<MetricCard title="总分" value={`${results.total}/30`} />
<MetricCard title="结果水平" value={levelText[results.level]} />
<MetricCard title="题目数" value="10题" />
</div>
<div className="mt-5 h-2 overflow-hidden rounded-full bg-gray-100">
<div
className="h-full rounded-full bg-indigo-600"
style={{ width: barWidth(results.total) }}
/>
</div>
</div>
<div className={`border p-6 shadow-sm ${levelClass}`}>
<h3 className="text-lg font-semibold mb-3"></h3>
<p className="text-sm">{levelDescription[results.level]}</p>
</div>
<div className="border bg-gray-50 p-4 text-sm text-gray-700">
Rosenberg
</div>
</div>
);
}
interface MetricCardProps {
title: string;
value: string;
}
function MetricCard({ title, value }: MetricCardProps) {
return (
<div className="rounded-lg bg-gray-50 p-4 text-center">
<div className="text-sm text-muted-foreground">{title}</div>
<div className="mt-1 text-2xl font-semibold text-indigo-600">
{value}
</div>
</div>
);
}
@@ -0,0 +1,140 @@
'use client';
import { calculateVIAResults, VIAStrength, VIAVirtue } from '../../test/private/VIACalculator';
interface VIAResultProps {
answers: string[];
}
const strengthDescriptions: Record<VIAStrength, string> = {
creativity: "用新颖有效的方式思考和行动。",
curiosity: "主动探索未知,持续提问和观察。",
judgment: "开放评估证据,避免草率判断。",
loveOfLearning: "从学习和掌握新知识中获得满足。",
perspective: "能整合经验,为自己或他人提供有用视角。",
bravery: "在压力、风险或恐惧中坚持重要行动。",
perseverance: "面对困难仍持续完成目标。",
honesty: "真实、可靠,不依赖伪装获得认可。",
zest: "带着能量、投入和生命力行动。",
love: "重视亲密、信任和双向关心。",
kindness: "愿意照顾他人并提供实际帮助。",
socialIntelligence: "理解他人情绪、动机和场景需求。",
teamwork: "愿意合作并承担团队责任。",
fairness: "重视公正、平等和不偏袒。",
leadership: "组织、协调并带动群体前进。",
forgiveness: "能在合适时机放下怨气,重新看待关系。",
humility: "不过度夸大自己,承认仍需学习。",
prudence: "行动前考虑风险、后果和边界。",
selfRegulation: "管理冲动、情绪和行为节奏。",
appreciation: "感受自然、艺术和卓越表现中的美。",
gratitude: "看见并珍惜已经拥有和被给予的东西。",
hope: "相信未来可以改善,并愿意为此行动。",
humor: "用轻松方式缓和压力、连接他人。",
spirituality: "感到自己与更大的意义、信念或整体有关联。",
};
const virtueDescriptions: Record<VIAVirtue, string> = {
wisdom: "认知探索、学习、判断和形成视角的优势。",
courage: "面对困难仍行动、坚持和保持真实的优势。",
humanity: "建立亲密、善意和人际理解的优势。",
justice: "合作、公平和组织群体行动的优势。",
temperance: "克制、审慎、谦逊和修复关系的优势。",
transcendence: "连接意义、美、希望、感恩和幽默的优势。",
};
function width(score: number) {
return `${Math.max(0, Math.min(100, ((score - 1) / 4) * 100))}%`;
}
export function VIAResult({ answers }: VIAResultProps) {
const results = calculateVIAResults(answers);
const signature = results.rankedStrengths.slice(0, 5);
return (
<div className="mt-6 space-y-6">
<div className="border bg-white p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">VIA </h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-5">
{signature.map((item, index) => (
<MetricCard key={item.id} title={`优势 ${index + 1}`} value={item.name} score={item.score.toFixed(2)} />
))}
</div>
</div>
<div className="border bg-blue-50 p-6 text-blue-900 shadow-sm">
<h3 className="text-lg font-semibold mb-3">
{signature.map((item) => item.name).join(" / ")}
</h3>
<p className="text-sm">
VIA结果重点看你最自然
</p>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{results.rankedVirtues.map((item) => (
<ScoreCard
key={item.id}
title={item.name}
score={item.score}
description={virtueDescriptions[item.id]}
color="bg-indigo-500"
/>
))}
</div>
<div className="border bg-white p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">24</h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{results.rankedStrengths.map((item) => (
<ScoreCard
key={item.id}
title={item.name}
score={item.score}
description={strengthDescriptions[item.id]}
color="bg-emerald-500"
/>
))}
</div>
</div>
<div className="border bg-gray-50 p-4 text-sm text-gray-700">
VIA-IS题库5
</div>
</div>
);
}
function MetricCard({ title, value, score }: { title: string; value: string; score: string }) {
return (
<div className="rounded-lg bg-gray-50 p-4 text-center">
<div className="text-sm text-muted-foreground">{title}</div>
<div className="mt-1 text-xl font-semibold text-indigo-600">{value}</div>
<div className="text-xs text-muted-foreground">{score} / 5</div>
</div>
);
}
function ScoreCard({
title,
score,
description,
color,
}: {
title: string;
score: number;
description: string;
color: string;
}) {
return (
<div className="border bg-white p-5 shadow-sm">
<div className="mb-3 flex items-center justify-between gap-3">
<h4 className="font-semibold">{title}</h4>
<span className="text-sm text-muted-foreground">{score.toFixed(2)} / 5</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-gray-100">
<div className={`h-full rounded-full ${color}`} style={{ width: width(score) }} />
</div>
<p className="mt-3 text-sm text-gray-700">{description}</p>
</div>
);
}
@@ -0,0 +1,104 @@
'use client';
import React from 'react';
import { calculateWHO5Results } from '../../test/private/WHO5Calculator';
interface WHO5ResultProps {
answers: string[];
}
const levelText = {
high: '幸福感较好',
moderate: '幸福感中等',
low: '幸福感偏低',
very_low: '幸福感明显偏低',
};
const levelDescription = {
high: '最近两周的积极情绪、活力和生活兴趣整体较好,可以继续保持目前有帮助的生活节奏。',
moderate: '整体幸福感处在中等区间,可以留意睡眠、压力、社交和日常恢复感的变化。',
low: '幸福感偏低,建议结合PHQ-9、GAD-7或DASS-21进一步了解情绪状态。',
very_low: '幸福感明显偏低,近期可能承受了较多压力或情绪困扰,建议尽快寻求可靠支持或专业评估。',
};
const questions = [
'快乐、心情愉快',
'平静和放松',
'精力充沛、充满活力',
'醒来时清新、休息充分',
'日常生活中有感兴趣的事情',
];
export function WHO5Result({ answers }: WHO5ResultProps) {
const answersMap: { [key: number]: string } = {};
answers.forEach((answer, index) => {
answersMap[index + 1] = answer;
});
const results = calculateWHO5Results({ answers: answersMap });
const levelClass = results.level === 'high'
? 'border-green-200 bg-green-50 text-green-800'
: results.level === 'moderate'
? 'border-blue-200 bg-blue-50 text-blue-800'
: results.level === 'low'
? 'border-yellow-200 bg-yellow-50 text-yellow-800'
: 'border-red-200 bg-red-50 text-red-800';
return (
<div className="mt-6 space-y-6">
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4">WHO-5 </h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<MetricCard title="原始总分" value={`${results.rawScore}/25`} />
<MetricCard title="百分制得分" value={`${results.percentageScore}/100`} />
<MetricCard title="结果水平" value={levelText[results.level]} />
</div>
</div>
<div className={`border rounded-lg p-6 shadow-sm ${levelClass}`}>
<h3 className="text-lg font-semibold mb-3"></h3>
<p className="text-sm">{levelDescription[results.level]}</p>
{results.needsAttention && (
<p className="text-sm mt-3 font-medium">
WHO-5130-1
</p>
)}
</div>
<div className="bg-white border rounded-lg p-6 shadow-sm">
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="space-y-3">
{results.itemScores.map((score, index) => (
<div key={questions[index]} className="flex items-center justify-between gap-4 p-3 bg-gray-50 rounded-lg">
<span className="text-sm font-medium">{index + 1}. {questions[index]}</span>
<span className={`text-lg font-semibold ${score <= 1 ? 'text-red-600' : score <= 3 ? 'text-yellow-600' : 'text-green-600'}`}>
{score}/5
</span>
</div>
))}
</div>
</div>
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
<p className="text-sm text-gray-700">
WHO-5
</p>
</div>
</div>
);
}
interface MetricCardProps {
title: string;
value: React.ReactNode;
}
function MetricCard({ title, value }: MetricCardProps) {
return (
<div className="bg-gray-50 rounded-lg p-4 flex flex-col items-center">
<span className="text-sm text-gray-500 mb-1">{title}</span>
<span className="text-2xl font-semibold text-indigo-600 text-center">{value}</span>
</div>
);
}
@@ -0,0 +1,43 @@
import { Questionnaire } from '@/types';
import { useScopedI18n } from '@/locales/client';
interface AnswerListProps {
questions: Questionnaire['questions'];
answers: string[]; // array of selected option values in order
renderOptions: (id: number) => { id: number; content: string; value: string }[];
}
export function AnswerList({ questions, answers, renderOptions }: AnswerListProps) {
const t = useScopedI18n('common');
if (!questions || questions.length === 0) return null;
return (
<div className="mt-6">
<h3 className="text-lg font-medium mb-3">{t('answerList.title')}</h3>
<div className="space-y-2">
{questions.map((q, idx) => {
const selectedValue = answers[idx];
const optionContent = selectedValue !== undefined ? (() => {
const opts = renderOptions(q.id) || [];
const found = opts.find(o => String(o.value) === String(selectedValue));
return found ? found.content : `${t('answerList.option')} ${selectedValue}`;
})() : t('answerList.unanswered');
return (
<div
key={q.id}
className="flex items-start gap-2 p-3 bg-gray-50 rounded-md text-sm"
>
<span className="font-medium">{idx + 1}. {q.content}</span>
<span className="ml-auto">
{selectedValue !== undefined ? `${optionContent}` : t('answerList.unanswered')}
</span>
</div>
);
})}
</div>
</div>
);
}
@@ -0,0 +1,133 @@
import Link from 'next/link';
import { ReactNode } from 'react';
import { Button } from '@/components/ui/button';
import { useScopedI18n } from '@/locales/client';
import { Copy, Download, FileText } from 'lucide-react';
import { toast } from 'sonner';
import { Questionnaire } from '@/types';
interface ResultContainerProps {
title: string;
id: string;
children: ReactNode;
questionnaire?: Questionnaire;
answers?: string[];
questionnaireResults?: Record<string, string>;
}
export function ResultContainer({ title, id, children, questionnaire, answers, questionnaireResults }: ResultContainerProps) {
const t = useScopedI18n(
'component.questionnaire.result.public.resultContainer'
);
const handleCopyResultLink = async () => {
try {
await navigator.clipboard.writeText(
`${window.location.origin}${window.location.pathname}`,
);
toast.success(t('copySuccess'));
} catch {
toast.error(t('copyError'));
}
};
const buildResultMarkdown = () => {
if (!questionnaire || !answers || !questionnaireResults) {
return null;
}
const currentTime = new Date().toLocaleString();
let resultData = `# ${t('copyTemplate.title')}\n\n`;
resultData += `## ${t('copyTemplate.basicInfo')}\n`;
resultData += `- ${t('copyTemplate.questionnaireName')}: ${questionnaire.title}\n`;
resultData += `- ${t('copyTemplate.questionnaireId')}: ${id}\n`;
resultData += `- ${t('copyTemplate.assessmentTime')}: ${currentTime}\n`;
resultData += `- ${t('copyTemplate.questionCount')}: ${questionnaire.questions.length}\n\n`;
resultData += `## ${t('copyTemplate.questionsAndAnswers')}\n`;
Object.entries(questionnaireResults).forEach(([question, answer], index) => {
resultData += `${index + 1}. ${question}\n ${t('copyTemplate.answer')}: ${answer}\n\n`;
});
resultData += `## ${t('copyTemplate.usage')}\n`;
resultData += `${t('copyTemplate.disclaimer')}\n\n`;
resultData += `${t('copyTemplate.source')}: ${t('copyTemplate.platform')}\n`;
resultData += `${t('copyTemplate.website')}: ${window.location.origin}\n`;
return resultData;
};
const handleCopyResultData = async () => {
const resultData = buildResultMarkdown();
if (!resultData) {
toast.error(t('copyResultDataError'));
return;
}
try {
await navigator.clipboard.writeText(resultData);
toast.success(t('copyResultDataSuccess'));
} catch {
toast.error(t('copyResultDataError'));
}
};
const handleDownloadResultData = () => {
const resultData = buildResultMarkdown();
if (!resultData) {
toast.error(t('downloadResultDataError'));
return;
}
const blob = new Blob([resultData], { type: 'text/markdown;charset=utf-8' });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = `${id}-result.md`;
anchor.click();
URL.revokeObjectURL(url);
toast.success(t('downloadResultDataSuccess'));
};
return (
<div className="flex justify-center items-center min-h-screen md:p-4 p-2">
<div className="max-w-6xl w-full bg-white rounded-lg shadow-lg md:p-8 p-4 border">
<h1 className="text-2xl font-bold mb-6">
{title} - {t('resultText')}
</h1>
<div className="mb-8">
<div className="space-y-6">{children}</div>
</div>
<div className="flex flex-col sm:flex-row sm:justify-between gap-4 mt-8">
<Button variant="outline" className="w-full sm:w-auto">
<Link href={`/questionnaire/${id}`}>{t('backToDetail')}</Link>
</Button>
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
<Button variant="outline" onClick={handleCopyResultLink} className="w-full sm:w-auto">
<Copy className="w-4 h-4 mr-2" />
{t('copyResultLink')}
</Button>
<Button
variant="outline"
onClick={handleCopyResultData}
className="w-full sm:w-auto"
>
<FileText className="w-4 h-4 mr-2" />
{t('copyResultData')}
</Button>
<Button
variant="outline"
onClick={handleDownloadResultData}
className="w-full sm:w-auto"
>
<Download className="w-4 h-4 mr-2" />
{t('downloadResultData')}
</Button>
</div>
</div>
</div>
</div>
);
}