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

This commit is contained in:
mikemoi
2026-06-22 22:59:01 +02:00
commit 9227c687fc
160 changed files with 16974 additions and 0 deletions
@@ -0,0 +1,89 @@
import { QuestionType } from "@/types";
/**
* ADHD Self-Report Scale calculator parameters interface
*/
interface ADHDCalculatorProps {
/** Answer data, key is question ID, value is selected score */
answers: { [key: number]: string };
/** Questions list */
questions: QuestionType[];
}
/**
* Calculate ADHD Self-Report Scale (ASRS-v1.1) results
*
* @param answers - User answer data, containing question ID and selected score
* @returns Calculation results, including total score, factor scores, severity level and other information
*/
export const calculateADHDResults = ({ answers }: ADHDCalculatorProps): any => {
// ADHD ASRS calculation logic
let totalScore = 0;
// Subscale score initialization: inattention and hyperactivity-impulsivity
let inattentionScore = 0; // Inattention subscale score (questions 1-9)
let hyperactivityScore = 0; // Hyperactivity-Impulsivity subscale score (questions 10-18)
// Part A screening questions (questions 1-6) - critical for screening
let partAScore = 0;
let partAPositive = 0; // Count of positive responses in Part A
// Calculate total score and subscale scores
Object.entries(answers).forEach(([questionId, score]) => {
const questionNum = parseInt(questionId);
const scoreValue = parseInt(score);
totalScore += scoreValue;
// Part A screening questions (questions 1-6)
if (questionNum >= 1 && questionNum <= 6) {
partAScore += scoreValue;
// Part A positive criteria: questions 1-4 score ≥2, questions 5-6 score ≥3
if ((questionNum >= 1 && questionNum <= 4 && scoreValue >= 2) ||
(questionNum >= 5 && questionNum <= 6 && scoreValue >= 3)) {
partAPositive++;
}
}
// Inattention subscale (questions 1-9)
if (questionNum >= 1 && questionNum <= 9) {
inattentionScore += scoreValue;
}
// Hyperactivity-Impulsivity subscale (questions 10-18)
else if (questionNum >= 10 && questionNum <= 18) {
hyperactivityScore += scoreValue;
}
});
// Determine screening result based on Part A
const screeningPositive = partAPositive >= 4;
// Determine severity level based on total score
let severity = "low";
if (totalScore >= 24 && totalScore <= 36) {
severity = "mild";
} else if (totalScore >= 37 && totalScore <= 54) {
severity = "moderate";
} else if (totalScore >= 55) {
severity = "high";
}
// Calculate factor scores
const factorScores: { [key: string]: number } = {
"inattention": inattentionScore,
"hyperactivity": hyperactivityScore,
"partA": partAScore
};
// Return complete calculation results
return {
totalScore,
factorScores,
positiveItemCount: Object.values(answers).filter(score => parseInt(score) >= 2).length,
positiveItemAverage: totalScore / Object.keys(answers).length,
isSevere: severity === "high",
severity,
screeningPositive,
partAPositive,
};
};
@@ -0,0 +1,40 @@
const avoidanceItems = [1, 2, 3, 4, 5, 6];
const anxietyItems = [7, 8, 9];
const reverseAvoidanceItems = new Set([1, 2, 3, 4]);
function scoreItem(answers: string[], item: number) {
const raw = Number(answers[item - 1] || 1);
return reverseAvoidanceItems.has(item) ? 8 - raw : raw;
}
function average(values: number[]) {
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
export function calculateAttachmentResults(answers: string[]) {
const avoidance = average(avoidanceItems.map((item) => scoreItem(answers, item)));
const anxiety = average(anxietyItems.map((item) => Number(answers[item - 1] || 1)));
let pattern:
| 'secure'
| 'preoccupied'
| 'dismissive'
| 'fearful' = 'secure';
const highAvoidance = avoidance >= 4;
const highAnxiety = anxiety >= 4;
if (highAvoidance && highAnxiety) {
pattern = 'fearful';
} else if (highAvoidance) {
pattern = 'dismissive';
} else if (highAnxiety) {
pattern = 'preoccupied';
}
return {
avoidance,
anxiety,
pattern,
};
}
@@ -0,0 +1,82 @@
import { QuestionType } from "@/types";
interface BDI2CalculatorProps {
answers: { [key: number]: string };
questions: QuestionType[];
}
export const calculateBDI2Results = ({ answers }: BDI2CalculatorProps): any => {
// BDI-II calculation logic
let totalScore = 0;
let suicidalIdeation = false;
// Calculate total score (simple sum)
Object.entries(answers).forEach(([questionId, score]) => {
const scoreValue = parseInt(score);
totalScore += scoreValue;
// Check question 9 (suicidal ideation)
if (parseInt(questionId) === 9 && scoreValue >= 1) {
suicidalIdeation = true;
}
});
// Determine depression severity level
let severity = "minimal";
if (totalScore >= 29) {
severity = "severe";
} else if (totalScore >= 20) {
severity = "moderate";
} else if (totalScore >= 14) {
severity = "mild";
}
// Analyze item scores
const itemAnalysis = Object.entries(answers).map(([questionId, score]) => ({
questionId: parseInt(questionId),
score: parseInt(score),
isHigh: parseInt(score) >= 2 // Scores of 2 or above are considered high score items
}));
const highScoreItems = itemAnalysis.filter(item => item.isHigh);
// Categorize analysis of different symptom domains
const emotionalItems = [1, 2, 4, 5, 10, 14]; // Emotional symptoms
const cognitiveItems = [3, 6, 8, 13, 19]; // Cognitive symptoms
const somaticItems = [15, 16, 18, 20, 21]; // Somatic symptoms
const behavioralItems = [7, 9, 11, 12, 17]; // Behavioral symptoms
const getSubscaleScore = (items: number[]) => {
return items.reduce((sum, itemId) => {
return sum + (answers[itemId] ? parseInt(answers[itemId]) : 0);
}, 0);
};
const emotionalScore = getSubscaleScore(emotionalItems);
const cognitiveScore = getSubscaleScore(cognitiveItems);
const somaticScore = getSubscaleScore(somaticItems);
const behavioralScore = getSubscaleScore(behavioralItems);
return {
totalScore,
severity,
suicidalIdeation,
itemAnalysis,
highScoreItemCount: highScoreItems.length,
emotionalScore,
cognitiveScore,
somaticScore,
behavioralScore,
factorScores: {
"emotional": emotionalScore,
"cognitive": cognitiveScore,
"somatic": somaticScore,
"behavioral": behavioralScore
},
positiveItemCount: highScoreItems.length,
positiveItemAverage: highScoreItems.length > 0
? highScoreItems.reduce((sum, item) => sum + item.score, 0) / highScoreItems.length
: 0,
isSevere: severity === "severe" || suicidalIdeation
};
};
@@ -0,0 +1,85 @@
import {
BigFiveDomain,
IpipNeoItem,
ipipNeo120Items,
ipipNeo300Items,
} from '@/questionairies/bigfive/neo-data';
const reverseItems = new Set([
2, 4, 6, 8, 10, 12, 16, 18, 19, 20, 22, 24, 26, 28, 29, 30, 32, 34,
36, 38, 39, 44, 46, 48, 49,
]);
const dimensions = {
extraversion: [1, 6, 11, 16, 21, 26, 31, 36, 41, 46],
agreeableness: [2, 7, 12, 17, 22, 27, 32, 37, 42, 47],
conscientiousness: [3, 8, 13, 18, 23, 28, 33, 38, 43, 48],
emotionalStability: [4, 9, 14, 19, 24, 29, 34, 39, 44, 49],
openness: [5, 10, 15, 20, 25, 30, 35, 40, 45, 50],
};
export function calculateBigFiveResults(answers: string[]) {
const scoreItem = (questionId: number) => {
const raw = Number(answers[questionId - 1] || 0);
if (!raw) return 0;
return reverseItems.has(questionId) ? 6 - raw : raw;
};
return Object.fromEntries(
Object.entries(dimensions).map(([key, items]) => {
const score = items.reduce((sum, item) => sum + scoreItem(item), 0);
return [
key,
{
score,
average: score / items.length,
},
];
}),
);
}
export const ipipNeoItemsByVersion = {
120: ipipNeo120Items,
300: ipipNeo300Items,
} as const;
export function calculateIpipNeoResults(
answers: string[],
items: IpipNeoItem[],
) {
const domainTotals = new Map<BigFiveDomain, { score: number; count: number }>();
const facetTotals = new Map<string, { score: number; count: number }>();
items.forEach((item, index) => {
const raw = Number(answers[index] || 0);
const score = raw ? (item.reverse ? 6 - raw : raw) : 0;
const domain = domainTotals.get(item.domain) || { score: 0, count: 0 };
domain.score += score;
domain.count += 1;
domainTotals.set(item.domain, domain);
const facet = facetTotals.get(item.facet) || { score: 0, count: 0 };
facet.score += score;
facet.count += 1;
facetTotals.set(item.facet, facet);
});
const toResults = (totals: Map<string, { score: number; count: number }>) =>
Object.fromEntries(
[...totals.entries()].map(([key, value]) => [
key,
{
score: value.score,
average: value.count ? value.score / value.count : 0,
itemCount: value.count,
},
]),
);
return {
domains: toResults(domainTotals),
facets: toResults(facetTotals),
};
}
@@ -0,0 +1,29 @@
const correctAnswers: Record<number, string> = {
1: 'B',
2: 'A',
3: 'B',
4: 'A',
5: 'B',
6: 'B',
7: 'B',
};
export function calculateCRTResults(answers: string[]) {
const items = answers.map((answer, index) => {
const questionId = index + 1;
return {
questionId,
selected: answer,
correct: correctAnswers[questionId],
isCorrect: answer === correctAnswers[questionId],
};
});
const score = items.filter((item) => item.isCorrect).length;
return {
score,
total: Object.keys(correctAnswers).length,
items,
};
}
@@ -0,0 +1,55 @@
export type CareerAnchor =
| "technical"
| "managerial"
| "autonomy"
| "security"
| "entrepreneurial"
| "service"
| "challenge"
| "lifestyle";
const anchorItems: Record<CareerAnchor, number[]> = {
technical: [1, 2, 3, 4, 5],
managerial: [6, 7, 8, 9, 10],
autonomy: [11, 12, 13, 14, 15],
security: [16, 17, 18, 19, 20],
entrepreneurial: [21, 22, 23, 24, 25],
service: [26, 27, 28, 29, 30],
challenge: [31, 32, 33, 34, 35],
lifestyle: [36, 37, 38, 39, 40],
};
export const anchorNames: Record<CareerAnchor, string> = {
technical: "技术/职能能力",
managerial: "综合管理能力",
autonomy: "自主/独立",
security: "安全/稳定",
entrepreneurial: "创业创造",
service: "服务/奉献",
challenge: "纯粹挑战",
lifestyle: "生活方式",
};
function average(values: number[]) {
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
export function calculateCareerAnchorsResults(answers: string[]) {
const scores = Object.fromEntries(
Object.entries(anchorItems).map(([anchor, items]) => [
anchor,
average(items.map((item) => Number(answers[item - 1] || 1))),
]),
) as Record<CareerAnchor, number>;
const ranked = (Object.keys(scores) as CareerAnchor[])
.map((id) => ({ id, name: anchorNames[id], score: scores[id] }))
.sort((a, b) => b.score - a.score);
return {
scores,
ranked,
primary: ranked[0],
secondary: ranked[1],
};
}
@@ -0,0 +1,95 @@
import { QuestionType } from "@/types";
interface DASS21CalculatorProps {
answers: { [key: number]: string };
questions: QuestionType[];
}
export const calculateDASS21Results = ({ answers }: DASS21CalculatorProps): any => {
// DASS-21 calculation logic
// Depression dimension items: 3, 5, 10, 13, 16, 17, 21
// Anxiety dimension items: 2, 4, 7, 9, 15, 19, 20
// Stress dimension items: 1, 6, 8, 11, 12, 14, 18
const depressionItems = [3, 5, 10, 13, 16, 17, 21];
const anxietyItems = [2, 4, 7, 9, 15, 19, 20];
const stressItems = [1, 6, 8, 11, 12, 14, 18];
let depressionScore = 0;
let anxietyScore = 0;
let stressScore = 0;
let totalScore = 0;
// Calculate dimension scores
Object.entries(answers).forEach(([questionId, score]) => {
const questionNum = parseInt(questionId);
const scoreValue = parseInt(score);
totalScore += scoreValue;
if (depressionItems.includes(questionNum)) {
depressionScore += scoreValue;
} else if (anxietyItems.includes(questionNum)) {
anxietyScore += scoreValue;
} else if (stressItems.includes(questionNum)) {
stressScore += scoreValue;
}
});
// DASS-21 scores need to be multiplied by 2 for comparison with DASS-42
const finalDepressionScore = depressionScore * 2;
const finalAnxietyScore = anxietyScore * 2;
const finalStressScore = stressScore * 2;
// Determine severity level for each dimension
const getDepressionSeverity = (score: number) => {
if (score <= 9) return "normal";
if (score <= 13) return "mild";
if (score <= 20) return "moderate";
if (score <= 27) return "severe";
return "extremely_severe";
};
const getAnxietySeverity = (score: number) => {
if (score <= 7) return "normal";
if (score <= 9) return "mild";
if (score <= 14) return "moderate";
if (score <= 19) return "severe";
return "extremely_severe";
};
const getStressSeverity = (score: number) => {
if (score <= 14) return "normal";
if (score <= 18) return "mild";
if (score <= 25) return "moderate";
if (score <= 33) return "severe";
return "extremely_severe";
};
const depressionSeverity = getDepressionSeverity(finalDepressionScore);
const anxietySeverity = getAnxietySeverity(finalAnxietyScore);
const stressSeverity = getStressSeverity(finalStressScore);
// Determine overall severity level
const isAnySevere = depressionSeverity === "severe" || depressionSeverity === "extremely_severe" ||
anxietySeverity === "severe" || anxietySeverity === "extremely_severe" ||
stressSeverity === "severe" || stressSeverity === "extremely_severe";
return {
totalScore,
depressionScore: finalDepressionScore,
anxietyScore: finalAnxietyScore,
stressScore: finalStressScore,
depressionSeverity,
anxietySeverity,
stressSeverity,
factorScores: {
"depression": finalDepressionScore,
"anxiety": finalAnxietyScore,
"stress": finalStressScore
},
positiveItemCount: Object.values(answers).filter(score => parseInt(score) >= 2).length,
positiveItemAverage: totalScore / Object.keys(answers).length,
isSevere: isAnySevere
};
};
@@ -0,0 +1,28 @@
const dimensions = {
machiavellianism: [1, 2, 3, 4, 5, 6, 7, 8, 9],
narcissism: [10, 11, 12, 13, 14, 15, 16, 17, 18],
psychopathy: [19, 20, 21, 22, 23, 24, 25, 26, 27],
} as const;
const reverseItems = new Set([4, 8, 11, 15, 17, 20, 25]);
function scoreItem(answers: string[], item: number) {
const raw = Number(answers[item - 1] || 1);
return reverseItems.has(item) ? 6 - raw : raw;
}
function average(scores: number[]) {
return scores.reduce((sum, score) => sum + score, 0) / scores.length;
}
function dimensionAverage(answers: string[], items: readonly number[]) {
return average(items.map((item) => scoreItem(answers, item)));
}
export function calculateDarkTriadResults(answers: string[]) {
return {
machiavellianism: dimensionAverage(answers, dimensions.machiavellianism),
narcissism: dimensionAverage(answers, dimensions.narcissism),
psychopathy: dimensionAverage(answers, dimensions.psychopathy),
};
}
@@ -0,0 +1,30 @@
const subscales = {
perspectiveTaking: [1, 2, 3, 4, 5, 6, 7],
empathicConcern: [8, 9, 10, 11, 12, 13, 14],
personalDistress: [15, 16, 17, 18, 19, 20, 21],
fantasy: [22, 23, 24, 25, 26, 27, 28],
} as const;
const reverseItems = new Set([2, 4, 6, 10, 13, 17, 20, 24, 26, 28]);
function scoreItem(answers: string[], item: number) {
const raw = Number(answers[item - 1] || 1);
return reverseItems.has(item) ? 6 - raw : raw;
}
function average(scores: number[]) {
return scores.reduce((sum, score) => sum + score, 0) / scores.length;
}
function subscaleAverage(answers: string[], items: readonly number[]) {
return average(items.map((item) => scoreItem(answers, item)));
}
export function calculateEmpathyResults(answers: string[]) {
return {
perspectiveTaking: subscaleAverage(answers, subscales.perspectiveTaking),
empathicConcern: subscaleAverage(answers, subscales.empathicConcern),
personalDistress: subscaleAverage(answers, subscales.personalDistress),
fantasy: subscaleAverage(answers, subscales.fantasy),
};
}
@@ -0,0 +1,44 @@
const dimensions = {
explorer: [1, 5, 9, 13, 17, 21, 25, 29, 33, 37],
builder: [2, 6, 10, 14, 18, 22, 26, 30, 34, 38],
director: [3, 7, 11, 15, 19, 23, 27, 31, 35, 39],
negotiator: [4, 8, 12, 16, 20, 24, 28, 32, 36, 40],
} as const;
export type FisherDimension = keyof typeof dimensions;
const dimensionNames: Record<FisherDimension, string> = {
explorer: "探索者",
builder: "建设者",
director: "指挥者",
negotiator: "协商者",
};
function scoreDimension(answers: string[], items: readonly number[]) {
return items.reduce((sum, item) => sum + Number(answers[item - 1] || 1), 0);
}
export function calculateFisherResults(answers: string[]) {
const scores = Object.fromEntries(
Object.entries(dimensions).map(([dimension, items]) => [
dimension,
scoreDimension(answers, items),
]),
) as Record<FisherDimension, number>;
const ranked = (Object.keys(scores) as FisherDimension[])
.map((id) => ({
id,
name: dimensionNames[id],
score: scores[id],
percentage: Math.round(((scores[id] - 10) / 30) * 100),
}))
.sort((a, b) => b.score - a.score);
return {
scores,
ranked,
primary: ranked[0],
secondary: ranked[1],
};
}
@@ -0,0 +1,49 @@
import { QuestionType } from "@/types";
interface GAD7CalculatorProps {
answers: { [key: number]: string };
questions: QuestionType[];
}
export const calculateGAD7Results = ({ answers }: GAD7CalculatorProps): any => {
// GAD-7 calculation logic
let totalScore = 0;
// Calculate total score (simple sum)
Object.entries(answers).forEach(([, score]) => {
const scoreValue = parseInt(score);
totalScore += scoreValue;
});
// Determine anxiety severity level
let severity = "minimal";
if (totalScore >= 15) {
severity = "severe";
} else if (totalScore >= 10) {
severity = "moderate";
} else if (totalScore >= 5) {
severity = "mild";
}
// Analyze item scores
const itemAnalysis = Object.entries(answers).map(([questionId, score]) => ({
questionId: parseInt(questionId),
score: parseInt(score),
isHigh: parseInt(score) >= 2 // Scores of 2 or above are considered high score items
}));
const highScoreItems = itemAnalysis.filter(item => item.isHigh);
return {
totalScore,
severity,
itemAnalysis,
highScoreItemCount: highScoreItems.length,
factorScores: {}, // GAD-7 is a single-factor scale
positiveItemCount: highScoreItems.length,
positiveItemAverage: highScoreItems.length > 0
? highScoreItems.reduce((sum, item) => sum + item.score, 0) / highScoreItems.length
: 0,
isSevere: severity === "severe" || severity === "moderate"
};
};
@@ -0,0 +1,109 @@
import { QuestionType } from "@/types";
/**
* Gender Dysphoria Questionnaire calculator parameters interface
*/
interface GDCalculatorProps {
/** Answer data, key is question ID, value is selected score */
answers: { [key: number]: string };
/** Questions list */
questions: QuestionType[];
}
/**
* Calculate Gender Dysphoria Questionnaire (GDQ) results
*
* @param answers - User answer data, containing question ID and selected score
* @returns Calculation results, including total score, factor scores, and other information
*/
export const calculateGDResults = ({ answers }: GDCalculatorProps): any => {
// Gender Dysphoria calculation logic
let totalRawScore = 0;
// Reverse-scored items (questions where agreement indicates comfort with assigned gender)
const reverseItems = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25];
// Calculate total score with reverse scoring
Object.entries(answers).forEach(([questionId, score]) => {
const questionNum = parseInt(questionId);
const scoreValue = parseInt(score);
if (reverseItems.includes(questionNum)) {
// Reverse score: 7 becomes 1, 6 becomes 2, etc.
totalRawScore += (8 - scoreValue);
} else {
totalRawScore += scoreValue;
}
});
// Calculate factor scores based on conceptual dimensions
let genderIdentityScore = 0; // Questions about internal gender identity
let socialRoleScore = 0; // Questions about social gender roles
let physicalDysphoriaScore = 0; // Questions about physical characteristics
let genderExpressionScore = 0; // Questions about gender expression
Object.entries(answers).forEach(([questionId, score]) => {
const questionNum = parseInt(questionId);
let scoreValue = parseInt(score);
// Apply reverse scoring if needed
if (reverseItems.includes(questionNum)) {
scoreValue = 8 - scoreValue;
}
// Assign to factor scores based on question content
if ([2, 4, 6, 17, 18, 20, 22, 24, 27].includes(questionNum)) {
genderIdentityScore += scoreValue;
} else if ([5, 7, 12, 19, 21, 26].includes(questionNum)) {
socialRoleScore += scoreValue;
} else if ([3, 9, 10, 13, 15, 16].includes(questionNum)) {
physicalDysphoriaScore += scoreValue;
} else if ([1, 8, 11, 14, 23, 25].includes(questionNum)) {
genderExpressionScore += scoreValue;
}
});
// Determine general interpretation level
const totalPossibleScore = Object.keys(answers).length * 7;
const scorePercentage = (totalRawScore / totalPossibleScore) * 100;
let interpretation = "low";
if (scorePercentage >= 30 && scorePercentage < 50) {
interpretation = "mild";
} else if (scorePercentage >= 50 && scorePercentage < 70) {
interpretation = "moderate";
} else if (scorePercentage >= 70) {
interpretation = "high";
}
// Calculate factor scores
const factorScores: { [key: string]: number } = {
"genderIdentity": genderIdentityScore,
"socialRole": socialRoleScore,
"physicalDysphoria": physicalDysphoriaScore,
"genderExpression": genderExpressionScore
};
// Count items with scores above neutral (>4 after reverse scoring)
const elevatedItems = Object.entries(answers).filter(([questionId, score]) => {
const questionNum = parseInt(questionId);
let scoreValue = parseInt(score);
if (reverseItems.includes(questionNum)) {
scoreValue = 8 - scoreValue;
}
return scoreValue > 4;
}).length;
// Return complete calculation results
return {
totalScore: totalRawScore,
factorScores,
positiveItemCount: elevatedItems,
positiveItemAverage: totalRawScore / Object.keys(answers).length,
isSevere: interpretation === "high",
interpretation,
scorePercentage: Math.round(scorePercentage),
};
};
@@ -0,0 +1,40 @@
const reverseItems = new Set([1, 3, 5, 6]);
const perseveranceItems = [2, 4, 7, 8];
const consistencyItems = [1, 3, 5, 6];
function scoreItem(answers: string[], item: number) {
const raw = Number(answers[item - 1] || 1);
return reverseItems.has(item) ? 6 - raw : raw;
}
function average(values: number[]) {
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
export function calculateGritResults(answers: string[]) {
const itemScores = Array.from({ length: 8 }, (_, index) =>
scoreItem(answers, index + 1),
);
const totalAverage = average(itemScores);
const perseveranceAverage = average(
perseveranceItems.map((item) => scoreItem(answers, item)),
);
const consistencyAverage = average(
consistencyItems.map((item) => scoreItem(answers, item)),
);
let level: 'low' | 'moderate' | 'high' = 'moderate';
if (totalAverage < 3) {
level = 'low';
} else if (totalAverage >= 4) {
level = 'high';
}
return {
itemScores,
totalAverage,
perseveranceAverage,
consistencyAverage,
level,
};
}
@@ -0,0 +1,41 @@
const domains = {
honestyHumility: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
emotionality: [11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
extraversion: [21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
agreeableness: [31, 32, 33, 34, 35, 36, 37, 38, 39, 40],
conscientiousness: [41, 42, 43, 44, 45, 46, 47, 48, 49, 50],
openness: [51, 52, 53, 54, 55, 56, 57, 58, 59, 60],
} as const;
const reverseItems = new Set([
2, 4, 6, 8, 10,
12, 14, 16, 18, 20,
22, 24, 26, 28, 30,
32, 34, 36, 38, 40,
42, 44, 46, 48, 50,
52, 54, 56, 58, 60,
]);
function scoreItem(answers: string[], item: number) {
const raw = Number(answers[item - 1] || 1);
return reverseItems.has(item) ? 6 - raw : raw;
}
function average(scores: number[]) {
return scores.reduce((sum, score) => sum + score, 0) / scores.length;
}
function domainAverage(answers: string[], items: readonly number[]) {
return average(items.map((item) => scoreItem(answers, item)));
}
export function calculateHEXACOResults(answers: string[]) {
return {
honestyHumility: domainAverage(answers, domains.honestyHumility),
emotionality: domainAverage(answers, domains.emotionality),
extraversion: domainAverage(answers, domains.extraversion),
agreeableness: domainAverage(answers, domains.agreeableness),
conscientiousness: domainAverage(answers, domains.conscientiousness),
openness: domainAverage(answers, domains.openness),
};
}
@@ -0,0 +1,49 @@
import { QuestionType } from "@/types";
interface ISICalculatorProps {
answers: { [key: number]: string };
questions: QuestionType[];
}
export const calculateISIResults = ({ answers }: ISICalculatorProps): any => {
// ISI calculation logic
let totalScore = 0;
// Calculate total score (simple sum)
Object.entries(answers).forEach(([, score]) => {
const scoreValue = parseInt(score);
totalScore += scoreValue;
});
// Determine insomnia severity level
let severity = "no_insomnia";
if (totalScore >= 22) {
severity = "severe";
} else if (totalScore >= 15) {
severity = "moderate";
} else if (totalScore >= 8) {
severity = "subthreshold";
}
// Analyze item scores
const itemAnalysis = Object.entries(answers).map(([questionId, score]) => ({
questionId: parseInt(questionId),
score: parseInt(score),
isHigh: parseInt(score) >= 3 // Scores of 3 or above are considered high score items
}));
const highScoreItems = itemAnalysis.filter(item => item.isHigh);
return {
totalScore,
severity,
itemAnalysis,
highScoreItemCount: highScoreItems.length,
factorScores: {},
positiveItemCount: highScoreItems.length,
positiveItemAverage: highScoreItems.length > 0
? highScoreItems.reduce((sum, item) => sum + item.score, 0) / highScoreItems.length
: 0,
isSevere: severity === "severe" || severity === "moderate"
};
};
@@ -0,0 +1,32 @@
const highStandardItems = [1, 4, 5, 10, 13];
const searchItems = [2, 3, 6, 8, 9, 12];
const difficultyItems = [7, 11];
function average(scores: number[]) {
return scores.reduce((sum, score) => sum + score, 0) / scores.length;
}
function scoreItems(answers: string[], items: number[]) {
return average(items.map((item) => Number(answers[item - 1] || 1)));
}
export function calculateMaximizerResults(answers: string[]) {
const itemScores = answers.slice(0, 13).map((answer) => Number(answer || 1));
const averageScore = average(itemScores);
let level: 'satisficer' | 'balanced' | 'maximizer' = 'balanced';
if (averageScore < 3.25) {
level = 'satisficer';
} else if (averageScore >= 4.75) {
level = 'maximizer';
}
return {
itemScores,
average: averageScore,
highStandards: scoreItems(answers, highStandardItems),
search: scoreItems(answers, searchItems),
difficulty: scoreItems(answers, difficultyItems),
level,
};
}
@@ -0,0 +1,92 @@
import { QuestionType } from "@/types";
/**
* Narcissistic Personality Inventory calculator parameters interface
*/
interface NPDCalculatorProps {
/** Answer data, key is question ID, value is selected score */
answers: { [key: number]: string };
/** Questions list */
questions: QuestionType[];
}
/**
* Calculate Narcissistic Personality Inventory (NPI-16) results
*
* @param answers - User answer data, containing question ID and selected score
* @returns Calculation results, including total score, factor scores, severity level and other information
*/
export const calculateNPDResults = ({ answers }: NPDCalculatorProps): any => {
// NPI-16 calculation logic
let totalScore = 0;
// Subscale score initialization based on NPI factors
let leadershipAuthorityScore = 0; // Leadership/Authority subscale
let grandioseExhibitionismScore = 0; // Grandiose Exhibitionism subscale
let entitlementScore = 0; // Entitlement subscale
// Calculate total score and subscale scores
Object.entries(answers).forEach(([questionId, score]) => {
const questionNum = parseInt(questionId);
const scoreValue = parseInt(score);
totalScore += scoreValue;
// Assign to subscales based on NPI-16 factor structure
// Leadership/Authority: questions 1, 8, 10, 11, 12
if ([1, 8, 10, 11, 12].includes(questionNum)) {
leadershipAuthorityScore += scoreValue;
}
// Grandiose Exhibitionism: questions 2, 3, 7, 15
else if ([2, 3, 7, 15].includes(questionNum)) {
grandioseExhibitionismScore += scoreValue;
}
// Entitlement: questions 4, 5, 6, 9, 13, 14, 16
else if ([4, 5, 6, 9, 13, 14, 16].includes(questionNum)) {
entitlementScore += scoreValue;
}
});
// Determine interpretation level based on total score
// Based on research, average scores typically range from 2-8 in general population
let interpretation = "low";
if (totalScore >= 4 && totalScore <= 7) {
interpretation = "average";
} else if (totalScore >= 8 && totalScore <= 11) {
interpretation = "above_average";
} else if (totalScore >= 12) {
interpretation = "high";
}
// Calculate percentile approximation
let percentile = 0;
if (totalScore <= 2) percentile = 25;
else if (totalScore <= 5) percentile = 50;
else if (totalScore <= 8) percentile = 75;
else if (totalScore <= 11) percentile = 90;
else percentile = 95;
// Calculate factor scores
const factorScores: { [key: string]: number } = {
"leadership": leadershipAuthorityScore,
"exhibitionism": grandioseExhibitionismScore,
"entitlement": entitlementScore
};
// Identify dominant traits
const dominantTrait = Object.entries(factorScores).reduce((a, b) =>
factorScores[a[0]] > factorScores[b[0]] ? a : b
)[0];
// Return complete calculation results
return {
totalScore,
factorScores,
positiveItemCount: totalScore, // For NPI, each item is either 0 or 1
positiveItemAverage: totalScore / Object.keys(answers).length,
isSevere: interpretation === "high",
interpretation,
percentile,
dominantTrait,
};
};
@@ -0,0 +1,39 @@
const reverseItems = new Set([3, 4, 5, 7, 8, 9, 12, 16, 17]);
export function calculateNeedForCognitionResults(answers: string[]) {
const itemScores = answers.slice(0, 18).map((answer, index) => {
const raw = Number(answer || 1);
const item = index + 1;
return reverseItems.has(item) ? 6 - raw : raw;
});
const total = itemScores.reduce((sum, score) => sum + score, 0);
const average = total / itemScores.length;
let level: 'low' | 'moderate' | 'high' = 'moderate';
if (average < 3) {
level = 'low';
} else if (average >= 4) {
level = 'high';
}
const thinkingItems = [1, 2, 6, 10, 11, 18].map(
(item) => itemScores[item - 1],
);
const challengeItems = [3, 4, 5, 7, 8, 9, 12, 13, 14, 15, 16, 17].map(
(item) => itemScores[item - 1],
);
return {
itemScores,
total,
average,
thinkingAverage:
thinkingItems.reduce((sum, score) => sum + score, 0) /
thinkingItems.length,
challengeAverage:
challengeItems.reduce((sum, score) => sum + score, 0) /
challengeItems.length,
level,
};
}
@@ -0,0 +1,97 @@
type EnneagramType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
const directKeys: Record<number, EnneagramType> = {
1: 1, 10: 1, 19: 1, 28: 1,
2: 2, 11: 2, 20: 2, 29: 2,
3: 3, 12: 3, 21: 3, 30: 3,
4: 4, 13: 4, 22: 4, 31: 4,
5: 5, 14: 5, 23: 5, 32: 5,
6: 6, 15: 6, 24: 6, 33: 6,
7: 7, 16: 7, 25: 7, 34: 7,
8: 8, 17: 8, 26: 8, 35: 8,
9: 9, 18: 9, 27: 9, 36: 9,
};
const bipolarKeys: Record<number, { left?: EnneagramType[]; right?: EnneagramType[] }> = {
37: { left: [8], right: [6] },
38: { left: [1], right: [4, 7] },
39: { left: [9], right: [3] },
40: { left: [7], right: [5] },
41: { left: [1], right: [7] },
42: { left: [2], right: [5] },
43: { left: [7, 8], right: [9] },
44: { left: [5], right: [2, 4] },
45: { left: [8], right: [2, 9] },
46: { right: [1] },
47: { right: [2] },
48: { left: [3] },
49: { right: [4] },
50: { right: [5] },
51: { right: [6] },
52: { left: [7] },
53: { left: [8] },
54: { left: [9] },
};
export const enneagramTypeInfo: Record<EnneagramType, { name: string; shortName: string; description: string }> = {
1: { name: '1号 改革者', shortName: '改革者', description: '追求正确、负责、自律和改进。' },
2: { name: '2号 助人者', shortName: '助人者', description: '重视关系、照顾他人和被需要。' },
3: { name: '3号 成就者', shortName: '成就者', description: '重视目标、效率、表现和认可。' },
4: { name: '4号 自我型', shortName: '自我型', description: '重视真实、独特、情绪深度和个人意义。' },
5: { name: '5号 探索者', shortName: '探索者', description: '重视知识、理解、独立和能力感。' },
6: { name: '6号 忠诚者', shortName: '忠诚者', description: '重视安全、支持、可靠性和风险预判。' },
7: { name: '7号 享乐者', shortName: '享乐者', description: '重视自由、可能性、新体验和快乐。' },
8: { name: '8号 挑战者', shortName: '挑战者', description: '重视力量、直接、掌控和保护边界。' },
9: { name: '9号 调停者', shortName: '调停者', description: '重视和谐、稳定、舒适和避免冲突。' },
};
function addScore(
scores: Record<EnneagramType, { total: number; count: number }>,
type: EnneagramType,
value: number,
) {
scores[type].total += value;
scores[type].count += 1;
}
export function calculateOEPSResults(answers: string[]) {
const scores = Object.fromEntries(
([1, 2, 3, 4, 5, 6, 7, 8, 9] as EnneagramType[]).map((type) => [
type,
{ total: 0, count: 0 },
]),
) as Record<EnneagramType, { total: number; count: number }>;
answers.forEach((answer, index) => {
const questionId = index + 1;
const value = Number(answer || 0);
if (!value) return;
const directType = directKeys[questionId];
if (directType) {
addScore(scores, directType, value);
return;
}
const key = bipolarKeys[questionId];
if (!key) return;
key.left?.forEach((type) => addScore(scores, type, 6 - value));
key.right?.forEach((type) => addScore(scores, type, value));
});
const ranked = (Object.entries(scores) as [string, { total: number; count: number }][])
.map(([type, score]) => ({
type: Number(type) as EnneagramType,
total: score.total,
count: score.count,
average: score.count ? score.total / score.count : 0,
...enneagramTypeInfo[Number(type) as EnneagramType],
}))
.sort((a, b) => b.average - a.average);
return {
ranked,
top: ranked[0],
};
}
@@ -0,0 +1,64 @@
import { QuestionType } from "@/types";
interface PHQ9CalculatorProps {
answers: { [key: number]: string };
questions: QuestionType[];
}
export const calculatePHQ9Results = ({ answers }: PHQ9CalculatorProps): any => {
// PHQ-9 calculation logic
let totalScore = 0;
let suicidalIdeation = false;
// Calculate total score (simple sum)
Object.entries(answers).forEach(([questionId, score]) => {
const scoreValue = parseInt(score);
totalScore += scoreValue;
// Check question 9 (suicidal ideation)
if (parseInt(questionId) === 9 && scoreValue >= 1) {
suicidalIdeation = true;
}
});
// Determine depression severity level
let severity = "minimal";
if (totalScore >= 20) {
severity = "severe";
} else if (totalScore >= 15) {
severity = "moderately_severe";
} else if (totalScore >= 10) {
severity = "moderate";
} else if (totalScore >= 5) {
severity = "mild";
}
// Analyze item scores
const itemAnalysis = Object.entries(answers).map(([questionId, score]) => ({
questionId: parseInt(questionId),
score: parseInt(score),
isHigh: parseInt(score) >= 2 // Scores of 2 or above are considered high score items
}));
const highScoreItems = itemAnalysis.filter(item => item.isHigh);
// Determine possibility of major depressive episode (at least 5 symptoms, including at least one of the first two)
const coreSymptoms = itemAnalysis.slice(0, 2).filter(item => item.score >= 2);
const otherSymptoms = itemAnalysis.slice(2).filter(item => item.score >= 2);
const majorDepressionCriteria = coreSymptoms.length >= 1 && (coreSymptoms.length + otherSymptoms.length) >= 5;
return {
totalScore,
severity,
suicidalIdeation,
itemAnalysis,
highScoreItemCount: highScoreItems.length,
majorDepressionCriteria,
factorScores: {}, // PHQ-9 is a single-factor scale
positiveItemCount: highScoreItems.length,
positiveItemAverage: highScoreItems.length > 0
? highScoreItems.reduce((sum, item) => sum + item.score, 0) / highScoreItems.length
: 0,
isSevere: severity === "severe" || severity === "moderately_severe" || suicidalIdeation
};
};
@@ -0,0 +1,86 @@
import { QuestionType } from "@/types";
interface PSS10CalculatorProps {
answers: { [key: number]: string };
questions: QuestionType[];
}
export const calculatePSS10Results = ({ answers }: PSS10CalculatorProps): any => {
// PSS-10 calculation logic
let totalScore = 0;
// Reverse scoring items (4, 5, 7, 8)
const reverseItems = [4, 5, 7, 8];
// Calculate total score
Object.entries(answers).forEach(([questionId, score]) => {
const questionNum = parseInt(questionId);
const scoreValue = parseInt(score);
if (reverseItems.includes(questionNum)) {
// Reverse scoring: 0->4, 1->3, 2->2, 3->1, 4->0
totalScore += (4 - scoreValue);
} else {
totalScore += scoreValue;
}
});
// Determine stress level (based on reference values from research literature)
let severity = "low";
if (totalScore >= 27) {
severity = "high";
} else if (totalScore >= 14) {
severity = "moderate";
}
// Analyze item scores
const itemAnalysis = Object.entries(answers).map(([questionId, score]) => {
const questionNum = parseInt(questionId);
const scoreValue = parseInt(score);
const isReverse = reverseItems.includes(questionNum);
const actualScore = isReverse ? (4 - scoreValue) : scoreValue;
return {
questionId: questionNum,
originalScore: scoreValue,
actualScore: actualScore,
isReverse: isReverse,
isHigh: actualScore >= 3 // Scores of 3 or above are considered high score items
};
});
const highScoreItems = itemAnalysis.filter(item => item.isHigh);
// Calculate subscale scores
const stressPerceptionItems = [1, 2, 3, 6, 9, 10]; // Stress perception items
const copingAbilityItems = [4, 5, 7, 8]; // Coping ability items
let stressPerceptionScore = 0;
let copingAbilityScore = 0;
itemAnalysis.forEach(item => {
if (stressPerceptionItems.includes(item.questionId)) {
stressPerceptionScore += item.actualScore;
} else if (copingAbilityItems.includes(item.questionId)) {
copingAbilityScore += item.actualScore;
}
});
return {
totalScore,
severity,
itemAnalysis,
highScoreItemCount: highScoreItems.length,
stressPerceptionScore, // Stress perception score (0-24 points)
copingAbilityScore, // Coping ability score (0-16 points)
factorScores: {
"stress_perception": stressPerceptionScore,
"coping_ability": copingAbilityScore
},
positiveItemCount: highScoreItems.length,
positiveItemAverage: highScoreItems.length > 0
? highScoreItems.reduce((sum, item) => sum + item.actualScore, 0) / highScoreItems.length
: 0,
isSevere: severity === "high"
};
};
@@ -0,0 +1,31 @@
export const riasecTypes = {
R: { name: '现实型', items: [1, 2, 3, 4, 5, 6, 7, 8] },
I: { name: '研究型', items: [9, 10, 11, 12, 13, 14, 15, 16] },
A: { name: '艺术型', items: [17, 18, 19, 20, 21, 22, 23, 24] },
S: { name: '社会型', items: [25, 26, 27, 28, 29, 30, 31, 32] },
E: { name: '企业型', items: [33, 34, 35, 36, 37, 38, 39, 40] },
C: { name: '常规型', items: [41, 42, 43, 44, 45, 46, 47, 48] },
} as const;
export function calculateRIASECResults(answers: string[]) {
const scores = Object.fromEntries(
Object.entries(riasecTypes).map(([code, type]) => {
const score = type.items.reduce(
(sum, item) => sum + Number(answers[item - 1] || 0),
0,
);
return [code, { score, average: score / type.items.length }];
}),
);
const ranking = Object.entries(scores).sort(
([codeA, resultA], [codeB, resultB]) =>
resultB.score - resultA.score || codeA.localeCompare(codeB),
);
return {
scores,
ranking,
hollandCode: ranking.slice(0, 3).map(([code]) => code).join(''),
};
}
@@ -0,0 +1,70 @@
import { QuestionType } from "@/types";
interface SCL90CalculatorProps {
answers: { [key: number]: string };
questions: QuestionType[];
}
export const calculateSCL90Results = ({ answers }: SCL90CalculatorProps): any => {
// SCL-90 calculation logic
const totalScore = Object.values(answers).reduce((sum, score) => sum + parseInt(score), 0);
const positiveItemCount = Object.values(answers).filter(score => parseInt(score) >= 2).length;
const negativeItemCount = Object.values(answers).filter(score => parseInt(score) === 1).length;
const positiveItemAverage = positiveItemCount > 0
? (totalScore - negativeItemCount) / positiveItemCount
: 0;
// SCL-90 factor grouping (based on standard factor structure)
const factorMapping: { [key: string]: number[] } = {
"somatization": [1, 4, 12, 27, 40, 42, 48, 49, 52, 53, 56, 58], // Somatization
"obsessive": [3, 9, 10, 28, 38, 45, 46, 51, 55, 65], // Obsessive-compulsive
"interpersonal": [6, 21, 34, 36, 37, 41, 61, 69, 73], // Interpersonal sensitivity
"depression": [5, 14, 15, 20, 22, 26, 29, 30, 31, 32, 54, 71, 79], // Depression
"anxiety": [2, 17, 23, 33, 39, 57, 72, 78, 80, 86], // Anxiety
"hostility": [11, 24, 63, 67, 74, 81], // Hostility
"phobic": [13, 25, 47, 50, 70, 75, 82], // Phobic anxiety
"paranoid": [8, 18, 43, 68, 76, 83], // Paranoid ideation
"psychotic": [7, 16, 35, 62, 77, 84, 85, 87, 88, 90], // Psychoticism
"other": [19, 44, 59, 60, 64, 66, 89] // Other
};
// Calculate factor scores
const factorScores: { [key: string]: number } = {};
Object.entries(factorMapping).forEach(([factor, questionIndexes]) => {
let factorSum = 0;
let validQuestionCount = 0;
questionIndexes.forEach((index: number) => {
if (answers[index]) {
factorSum += parseInt(answers[index]);
validQuestionCount++;
}
});
factorScores[factor] = validQuestionCount > 0
? factorSum / validQuestionCount
: 0;
});
const isSevere = totalScore > 160 || positiveItemCount > 43;
// Determine overall severity level
let severity = "normal";
if (totalScore >= 160 && positiveItemCount >= 43) {
severity = "severe";
} else if (totalScore >= 120 || positiveItemCount >= 30) {
severity = "moderate";
} else if (totalScore >= 90 || positiveItemCount >= 20) {
severity = "mild";
}
return {
totalScore,
factorScores,
positiveItemCount,
positiveItemAverage,
isSevere,
severity
};
};
@@ -0,0 +1,47 @@
import { QuestionType } from "@/types";
interface SDSCalculatorProps {
answers: { [key: number]: string };
questions: QuestionType[];
}
export const calculateSDSResults = ({ answers }: SDSCalculatorProps): any => {
// SDS Depression Self-rating Scale calculation logic
const reverseItems = [2, 5, 6, 11, 12, 14, 16, 17, 18, 20]; // Reverse scoring items
let totalScore = 0;
// Calculate total score
Object.entries(answers).forEach(([questionId, score]) => {
const questionNum = parseInt(questionId);
const scoreValue = parseInt(score);
if (reverseItems.includes(questionNum)) {
// Reverse scoring: 1->4, 2->3, 3->2, 4->1
totalScore += (5 - scoreValue);
} else {
totalScore += scoreValue;
}
});
// Calculate standard score
const standardScore = Math.round(totalScore * 1.25);
// Determine depression level
let severity = "normal";
if (standardScore >= 53 && standardScore <= 62) {
severity = "mild";
} else if (standardScore >= 63 && standardScore <= 72) {
severity = "moderate";
} else if (standardScore > 72) {
severity = "severe";
}
return {
totalScore: standardScore,
factorScores: {},
positiveItemCount: 0,
positiveItemAverage: 0,
isSevere: severity === "severe",
severity: severity
};
};
@@ -0,0 +1,112 @@
export type SchwartzValue =
| "selfDirection"
| "stimulation"
| "hedonism"
| "achievement"
| "power"
| "security"
| "conformity"
| "tradition"
| "benevolence"
| "universalism";
export type SchwartzHigherOrder =
| "opennessToChange"
| "conservation"
| "selfEnhancement"
| "selfTranscendence";
const valueItems: Record<SchwartzValue, number[]> = {
selfDirection: [1, 11, 21],
stimulation: [2, 12, 22],
hedonism: [3, 13, 23],
achievement: [4, 14, 24],
power: [5, 15, 25],
security: [6, 16, 26],
conformity: [7, 17, 27],
tradition: [8, 18, 28],
benevolence: [9, 19, 29],
universalism: [10, 20, 30],
};
export const valueNames: Record<SchwartzValue, string> = {
selfDirection: "自主",
stimulation: "刺激",
hedonism: "享乐",
achievement: "成就",
power: "权力",
security: "安全",
conformity: "遵从",
tradition: "传统",
benevolence: "仁慈",
universalism: "普世主义",
};
export const higherOrderNames: Record<SchwartzHigherOrder, string> = {
opennessToChange: "开放变化",
conservation: "保守稳定",
selfEnhancement: "自我提升",
selfTranscendence: "自我超越",
};
function average(values: number[]) {
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
function scoreItems(answers: string[], items: number[]) {
return average(items.map((item) => Number(answers[item - 1] || 1)));
}
export function calculateSchwartzResults(answers: string[]) {
const valueScores = Object.fromEntries(
Object.entries(valueItems).map(([value, items]) => [
value,
scoreItems(answers, items),
]),
) as Record<SchwartzValue, number>;
const higherOrderScores: Record<SchwartzHigherOrder, number> = {
opennessToChange: average([
valueScores.selfDirection,
valueScores.stimulation,
valueScores.hedonism,
]),
conservation: average([
valueScores.security,
valueScores.conformity,
valueScores.tradition,
]),
selfEnhancement: average([
valueScores.achievement,
valueScores.power,
valueScores.hedonism,
]),
selfTranscendence: average([
valueScores.benevolence,
valueScores.universalism,
]),
};
const rankedValues = (Object.keys(valueScores) as SchwartzValue[])
.map((id) => ({
id,
name: valueNames[id],
score: valueScores[id],
}))
.sort((a, b) => b.score - a.score);
const rankedHigherOrders = (Object.keys(higherOrderScores) as SchwartzHigherOrder[])
.map((id) => ({
id,
name: higherOrderNames[id],
score: higherOrderScores[id],
}))
.sort((a, b) => b.score - a.score);
return {
valueScores,
higherOrderScores,
rankedValues,
rankedHigherOrders,
};
}
@@ -0,0 +1,36 @@
const reverseItems = new Set([2, 3, 5, 6, 7, 8, 10, 11, 12]);
export function calculateSelfControlResults(answers: string[]) {
const itemScores = answers.slice(0, 13).map((answer, index) => {
const raw = Number(answer || 1);
const item = index + 1;
return reverseItems.has(item) ? 6 - raw : raw;
});
const total = itemScores.reduce((sum, score) => sum + score, 0);
const average = total / itemScores.length;
let level: 'low' | 'moderate' | 'high' = 'moderate';
if (average < 3) {
level = 'low';
} else if (average >= 4) {
level = 'high';
}
const impulseItems = [1, 2, 5, 10, 11].map((item) => itemScores[item - 1]);
const executionItems = [3, 4, 6, 7, 8, 9, 12, 13].map(
(item) => itemScores[item - 1],
);
return {
itemScores,
total,
average,
impulseAverage:
impulseItems.reduce((sum, score) => sum + score, 0) / impulseItems.length,
executionAverage:
executionItems.reduce((sum, score) => sum + score, 0) /
executionItems.length,
level,
};
}
@@ -0,0 +1,24 @@
const reverseItems = new Set([2, 5, 6, 8, 9]);
export function calculateSelfEsteemResults(answers: string[]) {
const itemScores = answers.slice(0, 10).map((answer, index) => {
const raw = Number(answer || 0);
const item = index + 1;
return reverseItems.has(item) ? 3 - raw : raw;
});
const total = itemScores.reduce((sum, score) => sum + score, 0);
let level: 'low' | 'moderate' | 'high' = 'moderate';
if (total <= 14) {
level = 'low';
} else if (total >= 25) {
level = 'high';
}
return {
total,
itemScores,
level,
};
}
@@ -0,0 +1,111 @@
export type VIAStrength =
| "creativity" | "curiosity" | "judgment" | "loveOfLearning" | "perspective"
| "bravery" | "perseverance" | "honesty" | "zest"
| "love" | "kindness" | "socialIntelligence"
| "teamwork" | "fairness" | "leadership"
| "forgiveness" | "humility" | "prudence" | "selfRegulation"
| "appreciation" | "gratitude" | "hope" | "humor" | "spirituality";
export type VIAVirtue = "wisdom" | "courage" | "humanity" | "justice" | "temperance" | "transcendence";
const strengthItems: Record<VIAStrength, number[]> = {
creativity: [1, 2],
curiosity: [3, 4],
judgment: [5, 6],
loveOfLearning: [7, 8],
perspective: [9, 10],
bravery: [11, 12],
perseverance: [13, 14],
honesty: [15, 16],
zest: [17, 18],
love: [19, 20],
kindness: [21, 22],
socialIntelligence: [23, 24],
teamwork: [25, 26],
fairness: [27, 28],
leadership: [29, 30],
forgiveness: [31, 32],
humility: [33, 34],
prudence: [35, 36],
selfRegulation: [37, 38],
appreciation: [39, 40],
gratitude: [41, 42],
hope: [43, 44],
humor: [45, 46],
spirituality: [47, 48],
};
const virtueStrengths: Record<VIAVirtue, VIAStrength[]> = {
wisdom: ["creativity", "curiosity", "judgment", "loveOfLearning", "perspective"],
courage: ["bravery", "perseverance", "honesty", "zest"],
humanity: ["love", "kindness", "socialIntelligence"],
justice: ["teamwork", "fairness", "leadership"],
temperance: ["forgiveness", "humility", "prudence", "selfRegulation"],
transcendence: ["appreciation", "gratitude", "hope", "humor", "spirituality"],
};
export const strengthNames: 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: "灵性",
};
export const virtueNames: Record<VIAVirtue, string> = {
wisdom: "智慧",
courage: "勇气",
humanity: "仁爱",
justice: "正义",
temperance: "节制",
transcendence: "超越",
};
function average(values: number[]) {
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
export function calculateVIAResults(answers: string[]) {
const strengthScores = Object.fromEntries(
Object.entries(strengthItems).map(([strength, items]) => [
strength,
average(items.map((item) => Number(answers[item - 1] || 1))),
]),
) as Record<VIAStrength, number>;
const virtueScores = Object.fromEntries(
Object.entries(virtueStrengths).map(([virtue, strengths]) => [
virtue,
average(strengths.map((strength) => strengthScores[strength])),
]),
) as Record<VIAVirtue, number>;
const rankedStrengths = (Object.keys(strengthScores) as VIAStrength[])
.map((id) => ({ id, name: strengthNames[id], score: strengthScores[id] }))
.sort((a, b) => b.score - a.score);
const rankedVirtues = (Object.keys(virtueScores) as VIAVirtue[])
.map((id) => ({ id, name: virtueNames[id], score: virtueScores[id] }))
.sort((a, b) => b.score - a.score);
return { strengthScores, virtueScores, rankedStrengths, rankedVirtues };
}
@@ -0,0 +1,32 @@
interface WHO5CalculatorProps {
answers: { [key: number]: string };
}
export const calculateWHO5Results = ({ answers }: WHO5CalculatorProps) => {
const itemScores = Array.from({ length: 5 }, (_, index) => {
const questionId = index + 1;
return Number.parseInt(answers[questionId] ?? "0", 10);
});
const rawScore = itemScores.reduce((sum, score) => sum + score, 0);
const percentageScore = rawScore * 4;
const hasVeryLowItem = itemScores.some((score) => score <= 1);
let level: "high" | "moderate" | "low" | "very_low" = "high";
if (rawScore <= 7) {
level = "very_low";
} else if (rawScore <= 12) {
level = "low";
} else if (rawScore <= 17) {
level = "moderate";
}
return {
rawScore,
percentageScore,
itemScores,
hasVeryLowItem,
level,
needsAttention: rawScore <= 12 || hasVeryLowItem,
};
};
@@ -0,0 +1,77 @@
import { QuestionType } from "@/types";
/**
* Yale-Brown Obsessive Compulsive Scale calculator parameters interface
*/
interface YBOCSCalculatorProps {
/** Answer data, key is question ID, value is selected score */
answers: { [key: number]: string };
/** Questions list */
questions: QuestionType[];
}
/**
* Calculate Yale-Brown Obsessive Compulsive Scale (Y-BOCS) results
*
* @param answers - User answer data, containing question ID and selected score
* @returns Calculation results, including total score, factor scores, severity level and other information
*/
export const calculateYBOCSResults = ({ answers }: YBOCSCalculatorProps): any => {
// Y-BOCS Obsessive Compulsive Scale calculation logic
let totalScore = 0;
// Subscale score initialization: obsessions (questions 1-5) and compulsions (questions 6-10)
let obsessionScore = 0; // Obsessions subscale score
let compulsionScore = 0; // Compulsions subscale score
// Calculate total score and subscale scores
Object.entries(answers).forEach(([questionId, score]) => {
const questionNum = parseInt(questionId); // Question number
const scoreValue = parseInt(score); // Selected score value
totalScore += scoreValue; // Accumulate total score
// Subscale score calculation: distinguish between obsessions and compulsions
if (questionNum >= 1 && questionNum <= 5) {
// Obsessions subscale (questions 1-5)
obsessionScore += scoreValue;
} else if (questionNum >= 6 && questionNum <= 10) {
// Compulsions subscale (questions 6-10)
compulsionScore += scoreValue;
}
});
// Determine severity level of obsessive-compulsive symptoms based on total score
// 0-7: Normal
// 8-15: Mild obsessive-compulsive symptoms
// 16-23: Moderate obsessive-compulsive symptoms
// 24-31: Severe obsessive-compulsive symptoms
// 32-40: Extremely severe obsessive-compulsive symptoms
let severity = "normal"; // Default to normal
if (totalScore >= 8 && totalScore <= 15) {
severity = "mild"; // Mild obsessive-compulsive symptoms
} else if (totalScore >= 16 && totalScore <= 23) {
severity = "moderate"; // Moderate obsessive-compulsive symptoms
} else if (totalScore >= 24 && totalScore <= 31) {
severity = "severe"; // Severe obsessive-compulsive symptoms
} else if (totalScore >= 32) {
severity = "extreme"; // Extremely severe obsessive-compulsive symptoms
}
// Calculate factor scores (obsessions subscale and compulsions subscale)
// These factor scores can be used to analyze symptom characteristics and intervention directions
const factorScores: { [key: string]: number } = {
"obsession": obsessionScore, // Obsessions score
"compulsion": compulsionScore // Compulsions score
};
// Return complete calculation results
return {
totalScore, // Total score
factorScores, // Factor scores (including obsessions and compulsions)
positiveItemCount: Object.values(answers).filter(score => parseInt(score) >= 1).length, // Positive item count (number of items with score ≥1)
positiveItemAverage: totalScore / Object.keys(answers).length, // Positive item average score
isSevere: severity === "severe" || severity === "extreme", // Whether it's severe or extremely severe symptoms
severity // Severity level classification
};
};