feat: 完善中文心理测评平台
This commit is contained in:
@@ -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 Five、RIASEC 和实际生活反馈综合理解。
|
||||
</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为 "{questionnaireId}" 的结果分析。
|
||||
请检查量表配置或联系开发人员。
|
||||
</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-5通常建议:原始总分低于13分,或任一题为0-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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user