feat: 升级结构化计分可信度
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import { useEffect, useState, useMemo, use, useRef } from 'react';
|
import { useEffect, useMemo, use, useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Questionnaire } from '@/types';
|
import { Questionnaire } from '@/types';
|
||||||
import Link from 'next/link';
|
|
||||||
import { ResultContainer } from '@/components/questionnaire/result/public/ResultContainer';
|
import { ResultContainer } from '@/components/questionnaire/result/public/ResultContainer';
|
||||||
import { AnswerList } from '@/components/questionnaire/result/public/AnswerList';
|
import { AnswerList } from '@/components/questionnaire/result/public/AnswerList';
|
||||||
import { ResultAnalysis } from '@/components/questionnaire/result/analysis/ResultAnalysis';
|
import { ResultAnalysis } from '@/components/questionnaire/result/analysis/ResultAnalysis';
|
||||||
@@ -12,8 +12,7 @@ import { useQuestionnaire } from '@/hooks/useQuestionnaire';
|
|||||||
import { useScopedI18n } from '@/locales/client';
|
import { useScopedI18n } from '@/locales/client';
|
||||||
import { loadResult } from '@/lib/result-storage';
|
import { loadResult } from '@/lib/result-storage';
|
||||||
import { AssessmentRecord } from '@/lib/assessment-types';
|
import { AssessmentRecord } from '@/lib/assessment-types';
|
||||||
import { getProfiles, getRecords, updateRecordAnalysis } from '@/lib/assessment-db';
|
import { getProfiles, getRecords } from '@/lib/assessment-db';
|
||||||
import { syncAnonymousRecord } from '@/lib/anonymous-client';
|
|
||||||
|
|
||||||
export default function QuestionnaireResultPage({
|
export default function QuestionnaireResultPage({
|
||||||
params,
|
params,
|
||||||
@@ -25,14 +24,10 @@ export default function QuestionnaireResultPage({
|
|||||||
const [decodedAnswers, setDecodedAnswers] = useState<string[] | null>(null);
|
const [decodedAnswers, setDecodedAnswers] = useState<string[] | null>(null);
|
||||||
const [record, setRecord] = useState<AssessmentRecord | null>(null);
|
const [record, setRecord] = useState<AssessmentRecord | null>(null);
|
||||||
const [profileName, setProfileName] = useState('未命名档案');
|
const [profileName, setProfileName] = useState('未命名档案');
|
||||||
const analysisRef = useRef<HTMLDivElement>(null);
|
|
||||||
const t = useScopedI18n('app.questionnaire.result');
|
const t = useScopedI18n('app.questionnaire.result');
|
||||||
|
|
||||||
// Get the questionnaire with specified id from questionnaire data
|
|
||||||
const questionnaire = useQuestionnaire(id) as Questionnaire;
|
const questionnaire = useQuestionnaire(id) as Questionnaire;
|
||||||
const recordId = record?.id;
|
|
||||||
|
|
||||||
// Load results from tab-local storage. Answers are intentionally not kept in the URL.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!questionnaire || !questionnaire.details) {
|
if (!questionnaire || !questionnaire.details) {
|
||||||
return;
|
return;
|
||||||
@@ -40,6 +35,7 @@ export default function QuestionnaireResultPage({
|
|||||||
|
|
||||||
const stored = loadResult(id);
|
const stored = loadResult(id);
|
||||||
setDecodedAnswers(stored?.answers || null);
|
setDecodedAnswers(stored?.answers || null);
|
||||||
|
|
||||||
async function loadRecord() {
|
async function loadRecord() {
|
||||||
if (stored?.profileId) {
|
if (stored?.profileId) {
|
||||||
const profiles = await getProfiles();
|
const profiles = await getProfiles();
|
||||||
@@ -51,28 +47,10 @@ export default function QuestionnaireResultPage({
|
|||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadRecord();
|
void loadRecord();
|
||||||
}, [id, questionnaire]);
|
}, [id, questionnaire]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!recordId || !analysisRef.current) return;
|
|
||||||
const timer = window.setTimeout(() => {
|
|
||||||
const text = analysisRef.current?.innerText.trim() || '';
|
|
||||||
if (!text) return;
|
|
||||||
if (record?.analysisText === text) return;
|
|
||||||
const updated = record ? { ...record, analysisText: text } : null;
|
|
||||||
setRecord(updated);
|
|
||||||
void updateRecordAnalysis(recordId, text);
|
|
||||||
if (updated) {
|
|
||||||
void syncAnonymousRecord(updated).catch((error) => {
|
|
||||||
console.error('Failed to sync anonymous record analysis:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
return () => window.clearTimeout(timer);
|
|
||||||
}, [record, recordId]);
|
|
||||||
|
|
||||||
// Construct question-option text pairs for copying result data
|
|
||||||
const questionnaireResults: Record<string, string> = useMemo(() => {
|
const questionnaireResults: Record<string, string> = useMemo(() => {
|
||||||
if (!questionnaire || !decodedAnswers) return {};
|
if (!questionnaire || !decodedAnswers) return {};
|
||||||
const obj: Record<string, string> = {};
|
const obj: Record<string, string> = {};
|
||||||
@@ -80,35 +58,34 @@ export default function QuestionnaireResultPage({
|
|||||||
const val = decodedAnswers[idx];
|
const val = decodedAnswers[idx];
|
||||||
if (val === undefined) return;
|
if (val === undefined) return;
|
||||||
const option = questionnaire.renderOptions(q.id).find(
|
const option = questionnaire.renderOptions(q.id).find(
|
||||||
(o) => String(o.value) === String(val)
|
(o) => String(o.value) === String(val),
|
||||||
);
|
);
|
||||||
obj[q.content] = option ? option.content : String(val);
|
obj[q.content] = option ? option.content : String(val);
|
||||||
});
|
});
|
||||||
return obj;
|
return obj;
|
||||||
}, [decodedAnswers, questionnaire]);
|
}, [decodedAnswers, questionnaire]);
|
||||||
|
|
||||||
// If data not found, show 404 page
|
|
||||||
if (!questionnaire || !questionnaire.details) {
|
if (!questionnaire || !questionnaire.details) {
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center min-h-screen">
|
<div className="flex min-h-screen items-center justify-center">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
<div className="h-12 w-12 animate-spin rounded-full border-b-2 border-blue-500" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!decodedAnswers || decodedAnswers.length !== questionnaire.questions.length) {
|
if (!decodedAnswers || decodedAnswers.length !== questionnaire.questions.length) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center min-h-screen md:p-4 p-2">
|
<div className="flex min-h-screen items-center justify-center p-2 md:p-4">
|
||||||
<div className="max-w-6xl w-full bg-white rounded-lg shadow-lg md:p-8 p-4 border">
|
<div className="w-full max-w-6xl rounded-lg border bg-white p-4 shadow-lg md:p-8">
|
||||||
<h1 className="text-2xl font-bold mb-6">
|
<h1 className="mb-6 text-2xl font-bold">
|
||||||
{questionnaire.title} - {t('resultNotFoundTitle')}
|
{questionnaire.title} - {t('resultNotFoundTitle')}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-700 mb-6">{t('resultNotFoundDesc')}</p>
|
<p className="mb-6 text-gray-700">{t('resultNotFoundDesc')}</p>
|
||||||
<Button>
|
<Button asChild>
|
||||||
<Link href={`/questionnaire/${id}`}>{t('retryTest')}</Link>
|
<Link href={`/questionnaire/${id}`}>{t('retryTest')}</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,8 +94,8 @@ export default function QuestionnaireResultPage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResultContainer
|
<ResultContainer
|
||||||
title={questionnaire.title}
|
title={questionnaire.title}
|
||||||
id={id}
|
id={id}
|
||||||
questionnaire={questionnaire}
|
questionnaire={questionnaire}
|
||||||
answers={decodedAnswers}
|
answers={decodedAnswers}
|
||||||
@@ -131,9 +108,7 @@ export default function QuestionnaireResultPage({
|
|||||||
answers={decodedAnswers}
|
answers={decodedAnswers}
|
||||||
renderOptions={questionnaire.renderOptions}
|
renderOptions={questionnaire.renderOptions}
|
||||||
/>
|
/>
|
||||||
<div ref={analysisRef}>
|
<ResultAnalysis questionnaireId={id} answers={decodedAnswers} />
|
||||||
<ResultAnalysis questionnaireId={id} answers={decodedAnswers} />
|
|
||||||
</div>
|
|
||||||
</ResultContainer>
|
</ResultContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,14 +199,17 @@ export function Questionnaire({
|
|||||||
answer: option?.content || value,
|
answer: option?.content || value,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
const scoreSummary = buildScoreSummary(id, resultAnswers);
|
||||||
const record = await addAssessmentRecord({
|
const record = await addAssessmentRecord({
|
||||||
profileId: profile.id,
|
profileId: profile.id,
|
||||||
questionnaireId: id,
|
questionnaireId: id,
|
||||||
questionnaireTitle: questionnaire.title,
|
questionnaireTitle: questionnaire.title,
|
||||||
|
questionnaireVersion: scoreSummary.questionnaireVersion,
|
||||||
|
scoreVersion: scoreSummary.scoreVersion,
|
||||||
category: questionnaire.category,
|
category: questionnaire.category,
|
||||||
completedAt,
|
completedAt,
|
||||||
answers: recordedAnswers,
|
answers: recordedAnswers,
|
||||||
scoreSummary: buildScoreSummary(id, resultAnswers),
|
scoreSummary,
|
||||||
retestSuitable: questionnaire.evaluation?.retestSuitable,
|
retestSuitable: questionnaire.evaluation?.retestSuitable,
|
||||||
recommendedInterval: questionnaire.evaluation?.recommendedInterval,
|
recommendedInterval: questionnaire.evaluation?.recommendedInterval,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,6 +32,41 @@ function displayDate(value: string) {
|
|||||||
return new Date(value).toLocaleString('zh-CN', { hour12: false });
|
return new Date(value).toLocaleString('zh-CN', { hour12: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function directionText(direction?: ScoreMetric['direction']) {
|
||||||
|
switch (direction) {
|
||||||
|
case 'risk':
|
||||||
|
return '高分代表风险或困扰升高';
|
||||||
|
case 'protective':
|
||||||
|
return '高分代表改善或保护因素更高';
|
||||||
|
case 'ability':
|
||||||
|
return '高分代表表现更好';
|
||||||
|
case 'trait':
|
||||||
|
return '高分代表该特质更明显';
|
||||||
|
case 'mixed':
|
||||||
|
return '高分含义需按维度查看';
|
||||||
|
default:
|
||||||
|
return '高分含义需结合量表说明';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeText(current: ScoreMetric, previous?: ScoreMetric) {
|
||||||
|
if (!previous) return '基线';
|
||||||
|
const diff = current.value - previous.value;
|
||||||
|
if (diff === 0) return '无变化';
|
||||||
|
const signed = `${diff > 0 ? '+' : ''}${diff}`;
|
||||||
|
switch (current.direction) {
|
||||||
|
case 'risk':
|
||||||
|
return diff > 0 ? `风险升高 ${signed}` : `风险降低 ${signed}`;
|
||||||
|
case 'protective':
|
||||||
|
case 'ability':
|
||||||
|
return diff > 0 ? `改善 ${signed}` : `下降 ${signed}`;
|
||||||
|
case 'trait':
|
||||||
|
return diff > 0 ? `特质增强 ${signed}` : `特质减弱 ${signed}`;
|
||||||
|
default:
|
||||||
|
return `变化 ${signed}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function MetricBar({ metric }: { metric: ScoreMetric }) {
|
function MetricBar({ metric }: { metric: ScoreMetric }) {
|
||||||
const width = metric.max ? Math.max(0, Math.min(100, metric.value / metric.max * 100)) : 0;
|
const width = metric.max ? Math.max(0, Math.min(100, metric.value / metric.max * 100)) : 0;
|
||||||
return (
|
return (
|
||||||
@@ -244,14 +279,15 @@ export function RecordsDashboard() {
|
|||||||
{trendGroups.map((group) => (
|
{trendGroups.map((group) => (
|
||||||
<div key={group[0].questionnaireId}>
|
<div key={group[0].questionnaireId}>
|
||||||
<div className="mb-3 flex flex-wrap items-baseline justify-between gap-2"><h2 className="text-lg font-semibold">{group[0].questionnaireTitle}</h2>{group[0].recommendedInterval && <span className="text-sm text-muted-foreground">建议间隔:{group[0].recommendedInterval}</span>}</div>
|
<div className="mb-3 flex flex-wrap items-baseline justify-between gap-2"><h2 className="text-lg font-semibold">{group[0].questionnaireTitle}</h2>{group[0].recommendedInterval && <span className="text-sm text-muted-foreground">建议间隔:{group[0].recommendedInterval}</span>}</div>
|
||||||
|
<p className="mb-3 text-sm text-muted-foreground">{group[0].scoreSummary.highScoreMeaning} · {directionText(group[0].scoreSummary.direction)}</p>
|
||||||
<div className="overflow-x-auto border">
|
<div className="overflow-x-auto border">
|
||||||
<table className="w-full min-w-[540px] text-sm"><thead className="bg-muted/50"><tr><th className="p-3 text-left">时间</th><th className="p-3 text-left">分数</th><th className="p-3 text-left">与上次相比</th></tr></thead>
|
<table className="w-full min-w-[640px] text-sm"><thead className="bg-muted/50"><tr><th className="p-3 text-left">时间</th><th className="p-3 text-left">分数</th><th className="p-3 text-left">高分方向</th><th className="p-3 text-left">与上次相比</th></tr></thead>
|
||||||
<tbody>{group.map((record, index) => { const metrics = record.scoreSummary.primary ? [record.scoreSummary.primary] : record.scoreSummary.metrics; const previous = group[index - 1]?.scoreSummary.primary; return <tr key={record.id} className="border-t"><td className="p-3">{displayDate(record.completedAt)}</td><td className="p-3">{metrics.map((item) => <span key={item.key} className="mr-4">{item.label} {item.value}{item.max ? `/${item.max}` : ''}</span>)}</td><td className="p-3">{record.scoreSummary.primary && previous ? `${record.scoreSummary.primary.value - previous.value > 0 ? '+' : ''}${record.scoreSummary.primary.value - previous.value}` : '基线'}</td></tr>; })}</tbody>
|
<tbody>{group.map((record, index) => { const metrics = record.scoreSummary.primary ? [record.scoreSummary.primary] : record.scoreSummary.metrics; const primary = record.scoreSummary.primary; const previous = group[index - 1]?.scoreSummary.primary; return <tr key={record.id} className="border-t"><td className="p-3">{displayDate(record.completedAt)}</td><td className="p-3">{metrics.map((item) => <span key={item.key} className="mr-4">{item.label} {item.value}{item.max ? `/${item.max}` : ''}</span>)}</td><td className="p-3">{directionText(primary?.direction || record.scoreSummary.direction)}</td><td className="p-3">{primary ? changeText(primary, previous) : '看各维度'}</td></tr>; })}</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<p className="text-sm text-muted-foreground">趋势只呈现分数变化,不自动判断改善或恶化;不同量表的高分方向并不相同。</p>
|
<p className="text-sm text-muted-foreground">趋势会按量表方向解释变化;敏感量表只作为自评追踪参考,不等同于诊断。</p>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+30
-1
@@ -10,6 +10,7 @@ import {
|
|||||||
encryptRecordForDevice,
|
encryptRecordForDevice,
|
||||||
isEncryptedRecord,
|
isEncryptedRecord,
|
||||||
} from '@/lib/record-crypto';
|
} from '@/lib/record-crypto';
|
||||||
|
import { buildScoreSummary } from '@/lib/score-summary';
|
||||||
|
|
||||||
const DB_NAME = 'mindscope';
|
const DB_NAME = 'mindscope';
|
||||||
const DB_VERSION = 1;
|
const DB_VERSION = 1;
|
||||||
@@ -47,6 +48,34 @@ function requestResult<T>(request: IDBRequest<T>): Promise<T> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRecord(record: AssessmentRecord): AssessmentRecord {
|
||||||
|
if (record.questionnaireVersion && record.scoreVersion && record.scoreSummary?.questionnaireVersion) {
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
const scoreSummary = buildScoreSummary(
|
||||||
|
record.questionnaireId,
|
||||||
|
record.answers.map((answer) => answer.value),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...record,
|
||||||
|
questionnaireVersion: record.questionnaireVersion || scoreSummary.questionnaireVersion,
|
||||||
|
scoreVersion: record.scoreVersion || scoreSummary.scoreVersion,
|
||||||
|
scoreSummary: {
|
||||||
|
...scoreSummary,
|
||||||
|
...record.scoreSummary,
|
||||||
|
questionnaireVersion: record.scoreSummary?.questionnaireVersion || scoreSummary.questionnaireVersion,
|
||||||
|
scoreVersion: record.scoreSummary?.scoreVersion || scoreSummary.scoreVersion,
|
||||||
|
min: record.scoreSummary?.min ?? scoreSummary.min,
|
||||||
|
max: record.scoreSummary?.max ?? scoreSummary.max,
|
||||||
|
reverseItems: record.scoreSummary?.reverseItems || scoreSummary.reverseItems,
|
||||||
|
direction: record.scoreSummary?.direction || scoreSummary.direction,
|
||||||
|
highScoreMeaning: record.scoreSummary?.highScoreMeaning || scoreSummary.highScoreMeaning,
|
||||||
|
thresholds: record.scoreSummary?.thresholds || scoreSummary.thresholds,
|
||||||
|
scoringStatus: record.scoreSummary?.scoringStatus || scoreSummary.scoringStatus,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function getActiveProfileId() {
|
export function getActiveProfileId() {
|
||||||
return localStorage.getItem(ACTIVE_PROFILE_KEY);
|
return localStorage.getItem(ACTIVE_PROFILE_KEY);
|
||||||
}
|
}
|
||||||
@@ -150,7 +179,7 @@ export async function getRecords(profileId?: string): Promise<AssessmentRecord[]
|
|||||||
db.close();
|
db.close();
|
||||||
|
|
||||||
const typedRecords = storedRecords as Array<AssessmentRecord | EncryptedAssessmentRecord>;
|
const typedRecords = storedRecords as Array<AssessmentRecord | EncryptedAssessmentRecord>;
|
||||||
const records = await Promise.all(typedRecords.map(decryptDeviceRecord));
|
const records = (await Promise.all(typedRecords.map(decryptDeviceRecord))).map(normalizeRecord);
|
||||||
const plaintextRecords = typedRecords.filter((record) => !isEncryptedRecord(record)) as AssessmentRecord[];
|
const plaintextRecords = typedRecords.filter((record) => !isEncryptedRecord(record)) as AssessmentRecord[];
|
||||||
if (plaintextRecords.length) {
|
if (plaintextRecords.length) {
|
||||||
await migratePlaintextRecords(plaintextRecords);
|
await migratePlaintextRecords(plaintextRecords);
|
||||||
|
|||||||
+45
-10
@@ -1,39 +1,74 @@
|
|||||||
import { AssessmentProfile, AssessmentRecord } from '@/lib/assessment-types';
|
import { AssessmentProfile, AssessmentRecord, ScoreThreshold } from '@/lib/assessment-types';
|
||||||
|
|
||||||
function date(value: string) {
|
function date(value: string) {
|
||||||
return new Date(value).toLocaleString('zh-CN', { hour12: false });
|
return new Date(value).toLocaleString('zh-CN', { hour12: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function thresholdsToText(thresholds: ScoreThreshold[]) {
|
||||||
|
if (!thresholds.length) return '无固定阈值';
|
||||||
|
return thresholds.map((item) => `${item.min} 分起:${item.label}`).join(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreRange(record: AssessmentRecord) {
|
||||||
|
const { min, max } = record.scoreSummary;
|
||||||
|
return max === undefined ? `最低 ${min}` : `${min} - ${max}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function recordToMarkdown(record: AssessmentRecord, profileName: string) {
|
export function recordToMarkdown(record: AssessmentRecord, profileName: string) {
|
||||||
const lines = [
|
const lines = [
|
||||||
`# ${record.questionnaireTitle}测评记录`, '',
|
`# ${record.questionnaireTitle}测评记录`,
|
||||||
|
'',
|
||||||
'## 基本信息',
|
'## 基本信息',
|
||||||
`- 档案:${profileName}`,
|
`- 档案:${profileName || '未命名档案'}`,
|
||||||
`- 测评时间:${date(record.completedAt)}`,
|
`- 测评时间:${date(record.completedAt)}`,
|
||||||
`- 量表编号:${record.questionnaireId}`,
|
`- 量表编号:${record.questionnaireId}`,
|
||||||
`- 题目数量:${record.answers.length}`, '',
|
`- 量表版本:${record.questionnaireVersion}`,
|
||||||
|
`- 计分版本:${record.scoreVersion}`,
|
||||||
|
`- 题目数量:${record.answers.length}`,
|
||||||
|
'',
|
||||||
|
'## 计分规则',
|
||||||
|
`- 分数范围:${scoreRange(record)}`,
|
||||||
|
`- 高分含义:${record.scoreSummary.highScoreMeaning}`,
|
||||||
|
`- 反向题:${record.scoreSummary.reverseItems.length ? record.scoreSummary.reverseItems.join('、') : '无'}`,
|
||||||
|
`- 临界值:${thresholdsToText(record.scoreSummary.thresholds)}`,
|
||||||
|
`- 计分状态:${record.scoreSummary.scoringStatus === 'structured' ? '结构化计分' : '原始分保存'}`,
|
||||||
|
'',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (record.scoreSummary.metrics.length) {
|
if (record.scoreSummary.metrics.length) {
|
||||||
lines.push('## 分数摘要');
|
lines.push('## 分数摘要');
|
||||||
record.scoreSummary.metrics.forEach((item) => {
|
record.scoreSummary.metrics.forEach((item) => {
|
||||||
lines.push(`- ${item.label}:${item.value}${item.max ? ` / ${item.max}` : ''}${item.level ? `(${item.level})` : ''}`);
|
const range = item.max !== undefined ? ` / ${item.max}` : '';
|
||||||
|
const level = item.level ? `(${item.level})` : '';
|
||||||
|
lines.push(`- ${item.label}:${item.value}${range}${level}`);
|
||||||
});
|
});
|
||||||
if (record.scoreSummary.note) lines.push('', record.scoreSummary.note);
|
if (record.scoreSummary.note) lines.push('', record.scoreSummary.note);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (record.analysisText) lines.push('## 结果说明', record.analysisText, '');
|
if (record.analysisText) lines.push('## 结果说明', record.analysisText, '');
|
||||||
|
|
||||||
lines.push('## 完整问答');
|
lines.push('## 完整问答');
|
||||||
record.answers.forEach((item, index) => lines.push(`${index + 1}. ${item.question}`, ` 回答:${item.answer}`, ''));
|
record.answers.forEach((item, index) => {
|
||||||
lines.push('## 使用说明', '本记录仅供自我了解、教育和研究参考,不构成医学或心理诊断。');
|
lines.push(`${index + 1}. ${item.question}`, ` 回答:${item.answer}`, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
lines.push(
|
||||||
|
'## 使用说明',
|
||||||
|
'本记录仅供自我了解、教育和研究参考,不构成医学或心理诊断。',
|
||||||
|
);
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function profileToMarkdown(profile: AssessmentProfile, records: AssessmentRecord[]) {
|
export function profileToMarkdown(profile: AssessmentProfile, records: AssessmentRecord[]) {
|
||||||
const lines = [
|
const lines = [
|
||||||
`# ${profile.name}的完整测评档案`, '',
|
`# ${profile.name}的完整测评档案`,
|
||||||
|
'',
|
||||||
`- 导出时间:${date(new Date().toISOString())}`,
|
`- 导出时间:${date(new Date().toISOString())}`,
|
||||||
`- 测评次数:${records.length}`, '',
|
`- 测评次数:${records.length}`,
|
||||||
'---', '',
|
'',
|
||||||
|
'---',
|
||||||
|
'',
|
||||||
];
|
];
|
||||||
records
|
records
|
||||||
.slice()
|
.slice()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export type ScoreDirection = 'risk' | 'protective' | 'ability' | 'trait' | 'mixed';
|
||||||
|
|
||||||
export interface AssessmentProfile {
|
export interface AssessmentProfile {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -5,18 +7,37 @@ export interface AssessmentProfile {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ScoreThreshold {
|
||||||
|
min: number;
|
||||||
|
label: string;
|
||||||
|
note?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ScoreMetric {
|
export interface ScoreMetric {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
value: number;
|
value: number;
|
||||||
|
min?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
level?: string;
|
level?: string;
|
||||||
|
direction?: ScoreDirection;
|
||||||
|
highScoreMeaning?: string;
|
||||||
|
thresholds?: ScoreThreshold[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScoreSummary {
|
export interface ScoreSummary {
|
||||||
primary?: ScoreMetric;
|
primary?: ScoreMetric;
|
||||||
metrics: ScoreMetric[];
|
metrics: ScoreMetric[];
|
||||||
note?: string;
|
note?: string;
|
||||||
|
questionnaireVersion: string;
|
||||||
|
scoreVersion: string;
|
||||||
|
min: number;
|
||||||
|
max?: number;
|
||||||
|
reverseItems: number[];
|
||||||
|
direction: ScoreDirection;
|
||||||
|
highScoreMeaning: string;
|
||||||
|
thresholds: ScoreThreshold[];
|
||||||
|
scoringStatus: 'structured' | 'raw';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecordedAnswer {
|
export interface RecordedAnswer {
|
||||||
@@ -31,6 +52,8 @@ export interface AssessmentRecord {
|
|||||||
profileId: string;
|
profileId: string;
|
||||||
questionnaireId: string;
|
questionnaireId: string;
|
||||||
questionnaireTitle: string;
|
questionnaireTitle: string;
|
||||||
|
questionnaireVersion: string;
|
||||||
|
scoreVersion: string;
|
||||||
category: string;
|
category: string;
|
||||||
completedAt: string;
|
completedAt: string;
|
||||||
answers: RecordedAnswer[];
|
answers: RecordedAnswer[];
|
||||||
@@ -57,6 +80,8 @@ export interface EncryptedAssessmentRecord {
|
|||||||
profileId: string;
|
profileId: string;
|
||||||
questionnaireId: string;
|
questionnaireId: string;
|
||||||
questionnaireTitle: string;
|
questionnaireTitle: string;
|
||||||
|
questionnaireVersion: string;
|
||||||
|
scoreVersion: string;
|
||||||
category: string;
|
category: string;
|
||||||
completedAt: string;
|
completedAt: string;
|
||||||
encrypted: true;
|
encrypted: true;
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ export async function encryptRecordForDevice(record: AssessmentRecord): Promise<
|
|||||||
profileId: record.profileId,
|
profileId: record.profileId,
|
||||||
questionnaireId: record.questionnaireId,
|
questionnaireId: record.questionnaireId,
|
||||||
questionnaireTitle: record.questionnaireTitle,
|
questionnaireTitle: record.questionnaireTitle,
|
||||||
|
questionnaireVersion: record.questionnaireVersion,
|
||||||
|
scoreVersion: record.scoreVersion,
|
||||||
category: record.category,
|
category: record.category,
|
||||||
completedAt: record.completedAt,
|
completedAt: record.completedAt,
|
||||||
encrypted: true,
|
encrypted: true,
|
||||||
@@ -136,6 +138,8 @@ export async function encryptRecordForAnonymousProfile(
|
|||||||
profileId: record.profileId,
|
profileId: record.profileId,
|
||||||
questionnaireId: record.questionnaireId,
|
questionnaireId: record.questionnaireId,
|
||||||
questionnaireTitle: record.questionnaireTitle,
|
questionnaireTitle: record.questionnaireTitle,
|
||||||
|
questionnaireVersion: record.questionnaireVersion,
|
||||||
|
scoreVersion: record.scoreVersion,
|
||||||
category: record.category,
|
category: record.category,
|
||||||
completedAt: record.completedAt,
|
completedAt: record.completedAt,
|
||||||
encrypted: true,
|
encrypted: true,
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ describe('buildScoreSummary', () => {
|
|||||||
it('applies PHQ-9 severity thresholds', () => {
|
it('applies PHQ-9 severity thresholds', () => {
|
||||||
const summary = buildScoreSummary('phq9', Array(9).fill('2'));
|
const summary = buildScoreSummary('phq9', Array(9).fill('2'));
|
||||||
expect(summary.primary).toMatchObject({ value: 18, max: 27, level: '中重度' });
|
expect(summary.primary).toMatchObject({ value: 18, max: 27, level: '中重度' });
|
||||||
|
expect(summary).toMatchObject({
|
||||||
|
questionnaireVersion: 'phq-9-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-phq9-v1',
|
||||||
|
scoringStatus: 'structured',
|
||||||
|
direction: 'risk',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('converts WHO-5 raw scores to a percentage', () => {
|
it('converts WHO-5 raw scores to a percentage', () => {
|
||||||
@@ -14,7 +20,8 @@ describe('buildScoreSummary', () => {
|
|||||||
|
|
||||||
it('reverse scores the positive PSS-10 items', () => {
|
it('reverse scores the positive PSS-10 items', () => {
|
||||||
const summary = buildScoreSummary('pss10', Array(10).fill('0'));
|
const summary = buildScoreSummary('pss10', Array(10).fill('0'));
|
||||||
expect(summary.primary).toMatchObject({ value: 16, max: 40, level: '中度' });
|
expect(summary.primary).toMatchObject({ value: 16, max: 40, level: '中等' });
|
||||||
|
expect(summary.reverseItems).toEqual([4, 5, 7, 8]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates all three DASS-21 dimensions independently', () => {
|
it('calculates all three DASS-21 dimensions independently', () => {
|
||||||
@@ -43,5 +50,6 @@ describe('buildScoreSummary', () => {
|
|||||||
const summary = buildScoreSummary('unknown', ['1', '2', '3']);
|
const summary = buildScoreSummary('unknown', ['1', '2', '3']);
|
||||||
expect(summary.primary).toMatchObject({ label: '原始作答总和', value: 6 });
|
expect(summary.primary).toMatchObject({ label: '原始作答总和', value: 6 });
|
||||||
expect(summary.note).toContain('不替代');
|
expect(summary.note).toContain('不替代');
|
||||||
|
expect(summary.scoringStatus).toBe('raw');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+172
-82
@@ -1,37 +1,53 @@
|
|||||||
import { ScoreMetric, ScoreSummary } from '@/lib/assessment-types';
|
import { ScoreDirection, ScoreMetric, ScoreSummary, ScoreThreshold } from '@/lib/assessment-types';
|
||||||
|
import { getScoringMetadata, MetricDefinition, QuestionnaireScoringMetadata } from '@/lib/scoring-metadata';
|
||||||
import {
|
import {
|
||||||
calculateBigFiveResults,
|
calculateBigFiveResults,
|
||||||
calculateIpipNeoResults,
|
calculateIpipNeoResults,
|
||||||
ipipNeoItemsByVersion,
|
ipipNeoItemsByVersion,
|
||||||
} from '@/components/questionnaire/test/private/BigFiveCalculator';
|
} from '@/components/questionnaire/test/private/BigFiveCalculator';
|
||||||
import { calculateHEXACOResults } from '@/components/questionnaire/test/private/HEXACOCalculator';
|
import { calculateHEXACOResults } from '@/components/questionnaire/test/private/HEXACOCalculator';
|
||||||
import { calculateRIASECResults, riasecTypes } from '@/components/questionnaire/test/private/RIASECCalculator';
|
import { calculateRIASECResults } from '@/components/questionnaire/test/private/RIASECCalculator';
|
||||||
import {
|
import {
|
||||||
calculateSchwartzResults,
|
calculateSchwartzResults,
|
||||||
higherOrderNames,
|
higherOrderNames,
|
||||||
} from '@/components/questionnaire/test/private/SchwartzCalculator';
|
} from '@/components/questionnaire/test/private/SchwartzCalculator';
|
||||||
|
|
||||||
const levels: Record<string, string> = {
|
const fallbackMetadata: QuestionnaireScoringMetadata = {
|
||||||
minimal: '极轻或无',
|
questionnaireVersion: 'unknown-questionnaire-v1',
|
||||||
mild: '轻度',
|
scoreVersion: 'mindscope-raw-v1',
|
||||||
moderate: '中度',
|
min: 0,
|
||||||
moderately_severe: '中重度',
|
reverseItems: [],
|
||||||
severe: '重度',
|
direction: 'trait',
|
||||||
normal: '正常范围',
|
highScoreMeaning: '高分代表原始作答总和更高,具体含义需要结合量表说明查看。',
|
||||||
extremely_severe: '极重度',
|
thresholds: [],
|
||||||
low: '较低',
|
note: '该量表暂以结构化原始分保存,用于保留完整记录与后续复核,不替代量表结果页中的正式解释。',
|
||||||
high: '较高',
|
|
||||||
subthreshold: '亚阈值',
|
|
||||||
no_insomnia: '无明显失眠',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const metric = (key: string, label: string, value: number, max?: number, level?: string): ScoreMetric => ({
|
const labelMap: Record<string, string> = {
|
||||||
key,
|
extraversion: '外向性',
|
||||||
label,
|
agreeableness: '宜人性',
|
||||||
value,
|
conscientiousness: '尽责性',
|
||||||
max,
|
emotionalStability: '情绪稳定性',
|
||||||
level: level ? levels[level] || level : undefined,
|
openness: '开放性',
|
||||||
});
|
neuroticism: '神经质',
|
||||||
|
honestyHumility: '诚实谦逊',
|
||||||
|
emotionality: '情绪性',
|
||||||
|
depression: '抑郁',
|
||||||
|
anxiety: '焦虑',
|
||||||
|
stress: '压力',
|
||||||
|
opennessToChange: '开放变化',
|
||||||
|
conservation: '保守稳定',
|
||||||
|
selfEnhancement: '自我提升',
|
||||||
|
selfTranscendence: '自我超越',
|
||||||
|
R: '现实型',
|
||||||
|
I: '研究型',
|
||||||
|
A: '艺术型',
|
||||||
|
S: '社会型',
|
||||||
|
E: '企业型',
|
||||||
|
C: '常规型',
|
||||||
|
total: '总分',
|
||||||
|
raw: '原始作答总和',
|
||||||
|
};
|
||||||
|
|
||||||
function sum(answers: string[]) {
|
function sum(answers: string[]) {
|
||||||
return answers.reduce((total, answer) => total + (Number(answer) || 0), 0);
|
return answers.reduce((total, answer) => total + (Number(answer) || 0), 0);
|
||||||
@@ -41,106 +57,180 @@ function keyed(answers: string[]) {
|
|||||||
return Object.fromEntries(answers.map((answer, index) => [index + 1, answer]));
|
return Object.fromEntries(answers.map((answer, index) => [index + 1, answer]));
|
||||||
}
|
}
|
||||||
|
|
||||||
function severity(score: number, thresholds: Array<[number, string]>) {
|
function metadataFor(questionnaireId: string) {
|
||||||
return thresholds.reduce((current, [minimum, label]) => score >= minimum ? label : current, thresholds[0][1]);
|
return getScoringMetadata(questionnaireId) || {
|
||||||
|
...fallbackMetadata,
|
||||||
|
questionnaireVersion: `${questionnaireId || 'unknown'}-v1`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function thresholdLabel(value: number, thresholds: ScoreThreshold[]) {
|
||||||
|
if (!thresholds.length) return undefined;
|
||||||
|
return thresholds.reduce((current, threshold) => (
|
||||||
|
value >= threshold.min ? threshold.label : current
|
||||||
|
), thresholds[0].label);
|
||||||
|
}
|
||||||
|
|
||||||
|
function metricDefinition(meta: QuestionnaireScoringMetadata, key: string): MetricDefinition | undefined {
|
||||||
|
return meta.metrics?.find((item) => item.key === key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeMetric(
|
||||||
|
meta: QuestionnaireScoringMetadata,
|
||||||
|
key: string,
|
||||||
|
label: string,
|
||||||
|
value: number,
|
||||||
|
overrides: Partial<ScoreMetric> = {},
|
||||||
|
): ScoreMetric {
|
||||||
|
const definition = metricDefinition(meta, key);
|
||||||
|
const min = overrides.min ?? definition?.min ?? meta.min;
|
||||||
|
const max = overrides.max ?? definition?.max ?? meta.max;
|
||||||
|
const thresholds = overrides.thresholds ?? definition?.thresholds ?? meta.thresholds;
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
direction: overrides.direction ?? definition?.direction ?? meta.direction,
|
||||||
|
highScoreMeaning: overrides.highScoreMeaning ?? definition?.highScoreMeaning ?? meta.highScoreMeaning,
|
||||||
|
thresholds,
|
||||||
|
level: overrides.level ?? thresholdLabel(value, thresholds),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalize(
|
||||||
|
questionnaireId: string,
|
||||||
|
metrics: ScoreMetric[],
|
||||||
|
primary?: ScoreMetric,
|
||||||
|
options: { note?: string; scoringStatus?: ScoreSummary['scoringStatus'] } = {},
|
||||||
|
): ScoreSummary {
|
||||||
|
const meta = metadataFor(questionnaireId);
|
||||||
|
return {
|
||||||
|
primary,
|
||||||
|
metrics,
|
||||||
|
note: options.note ?? meta.note,
|
||||||
|
questionnaireVersion: meta.questionnaireVersion,
|
||||||
|
scoreVersion: meta.scoreVersion,
|
||||||
|
min: meta.min,
|
||||||
|
max: meta.max,
|
||||||
|
reverseItems: meta.reverseItems,
|
||||||
|
direction: meta.direction,
|
||||||
|
highScoreMeaning: meta.highScoreMeaning,
|
||||||
|
thresholds: meta.thresholds,
|
||||||
|
scoringStatus: options.scoringStatus ?? 'structured',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildScoreSummary(questionnaireId: string, answers: string[]): ScoreSummary {
|
export function buildScoreSummary(questionnaireId: string, answers: string[]): ScoreSummary {
|
||||||
|
const meta = metadataFor(questionnaireId);
|
||||||
|
|
||||||
if (questionnaireId === 'bigfive') {
|
if (questionnaireId === 'bigfive') {
|
||||||
const result = calculateBigFiveResults(answers);
|
const result = calculateBigFiveResults(answers);
|
||||||
const names: Record<string, string> = {
|
const metrics = Object.entries(result).map(([key, value]) => (
|
||||||
extraversion: '外向性',
|
makeMetric(meta, key, labelMap[key] || key, value.score, { min: 10, max: 50 })
|
||||||
agreeableness: '宜人性',
|
));
|
||||||
conscientiousness: '尽责性',
|
return finalize(questionnaireId, metrics);
|
||||||
emotionalStability: '情绪稳定性',
|
|
||||||
openness: '开放性',
|
|
||||||
};
|
|
||||||
return { metrics: Object.entries(result).map(([key, value]) => metric(key, names[key], value.score, 50)) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (questionnaireId === 'bigfive-120' || questionnaireId === 'bigfive-300') {
|
if (questionnaireId === 'bigfive-120' || questionnaireId === 'bigfive-300') {
|
||||||
const version = questionnaireId === 'bigfive-120' ? 120 : 300;
|
const version = questionnaireId === 'bigfive-120' ? 120 : 300;
|
||||||
const result = calculateIpipNeoResults(answers, [...ipipNeoItemsByVersion[version]]);
|
const result = calculateIpipNeoResults(answers, [...ipipNeoItemsByVersion[version]]);
|
||||||
const names: Record<string, string> = {
|
const perDomainMax = version === 120 ? 120 : 300;
|
||||||
neuroticism: '神经质',
|
const metrics = Object.entries(result.domains).map(([key, value]) => (
|
||||||
extraversion: '外向性',
|
makeMetric(meta, key, labelMap[key] || key, value.score, {
|
||||||
openness: '开放性',
|
min: value.itemCount,
|
||||||
agreeableness: '宜人性',
|
max: perDomainMax,
|
||||||
conscientiousness: '尽责性',
|
})
|
||||||
};
|
));
|
||||||
const max = version === 120 ? 120 : 300;
|
return finalize(questionnaireId, metrics);
|
||||||
return { metrics: Object.entries(result.domains).map(([key, value]) => metric(key, names[key], value.score, max)) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (questionnaireId === 'hexaco') {
|
if (questionnaireId === 'hexaco') {
|
||||||
const names: Record<string, string> = {
|
const metrics = Object.entries(calculateHEXACOResults(answers)).map(([key, value]) => (
|
||||||
honestyHumility: '诚实谦逊',
|
makeMetric(meta, key, labelMap[key] || key, Number(value.toFixed(2)), { min: 1, max: 5 })
|
||||||
emotionality: '情绪性',
|
));
|
||||||
extraversion: '外向性',
|
return finalize(questionnaireId, metrics);
|
||||||
agreeableness: '宜人性',
|
|
||||||
conscientiousness: '尽责性',
|
|
||||||
openness: '开放性',
|
|
||||||
};
|
|
||||||
return { metrics: Object.entries(calculateHEXACOResults(answers)).map(([key, value]) => metric(key, names[key], Number(value.toFixed(2)), 5)) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (questionnaireId === 'riasec') {
|
if (questionnaireId === 'riasec') {
|
||||||
const result = calculateRIASECResults(answers);
|
const result = calculateRIASECResults(answers);
|
||||||
return {
|
const metrics = Object.entries(result.scores).map(([key, value]) => (
|
||||||
primary: metric('holland', `霍兰德代码 ${result.hollandCode}`, result.ranking[0][1].score, 40),
|
makeMetric(meta, key, labelMap[key] || key, value.score, { min: 0, max: 40 })
|
||||||
metrics: Object.entries(result.scores).map(([key, value]) => metric(key, riasecTypes[key as keyof typeof riasecTypes].name, value.score, 40)),
|
));
|
||||||
};
|
const primary = makeMetric(meta, 'holland', `霍兰德代码:${result.hollandCode}`, result.ranking[0][1].score, {
|
||||||
|
min: 0,
|
||||||
|
max: 40,
|
||||||
|
});
|
||||||
|
return finalize(questionnaireId, metrics, primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (questionnaireId === 'schwartz') {
|
if (questionnaireId === 'schwartz') {
|
||||||
const result = calculateSchwartzResults(answers);
|
const result = calculateSchwartzResults(answers);
|
||||||
return { metrics: Object.entries(result.higherOrderScores).map(([key, value]) => metric(key, higherOrderNames[key as keyof typeof higherOrderNames], Number(value.toFixed(2)), 5)) };
|
const metrics = Object.entries(result.higherOrderScores).map(([key, value]) => (
|
||||||
}
|
makeMetric(meta, key, higherOrderNames[key as keyof typeof higherOrderNames] || labelMap[key] || key, Number(value.toFixed(2)), {
|
||||||
|
min: 1,
|
||||||
const raw = sum(answers);
|
max: 5,
|
||||||
const single: Record<string, { label: string; max: number; score?: number; level?: string }> = {
|
})
|
||||||
phq9: { label: 'PHQ-9 总分', max: 27, level: severity(raw, [[0, 'minimal'], [5, 'mild'], [10, 'moderate'], [15, 'moderately_severe'], [20, 'severe']]) },
|
));
|
||||||
gad7: { label: 'GAD-7 总分', max: 21, level: severity(raw, [[0, 'minimal'], [5, 'mild'], [10, 'moderate'], [15, 'severe']]) },
|
return finalize(questionnaireId, metrics);
|
||||||
isi: { label: 'ISI 总分', max: 28, level: severity(raw, [[0, 'no_insomnia'], [8, 'subthreshold'], [15, 'moderate'], [22, 'severe']]) },
|
|
||||||
bdi2: { label: 'BDI-II 总分', max: 63, level: severity(raw, [[0, 'minimal'], [14, 'mild'], [20, 'moderate'], [29, 'severe']]) },
|
|
||||||
who5: { label: 'WHO-5 百分制得分', max: 100, score: raw * 4 },
|
|
||||||
};
|
|
||||||
|
|
||||||
if (single[questionnaireId]) {
|
|
||||||
const item = single[questionnaireId];
|
|
||||||
const primary = metric('total', item.label, item.score ?? raw, item.max, item.level);
|
|
||||||
return { primary, metrics: [primary] };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (questionnaireId === 'dass21') {
|
if (questionnaireId === 'dass21') {
|
||||||
|
const answerMap = keyed(answers);
|
||||||
const groups = {
|
const groups = {
|
||||||
depression: [3, 5, 10, 13, 16, 17, 21],
|
depression: [3, 5, 10, 13, 16, 17, 21],
|
||||||
anxiety: [2, 4, 7, 9, 15, 19, 20],
|
anxiety: [2, 4, 7, 9, 15, 19, 20],
|
||||||
stress: [1, 6, 8, 11, 12, 14, 18],
|
stress: [1, 6, 8, 11, 12, 14, 18],
|
||||||
};
|
};
|
||||||
const names = { depression: '抑郁', anxiety: '焦虑', stress: '压力' };
|
const metrics = Object.entries(groups).map(([key, ids]) => {
|
||||||
return { metrics: Object.entries(groups).map(([key, ids]) => metric(key, names[key as keyof typeof names], ids.reduce((total, id) => total + Number(keyed(answers)[id] || 0), 0) * 2, 42)) };
|
const value = ids.reduce((total, id) => total + Number(answerMap[id] || 0), 0) * 2;
|
||||||
|
return makeMetric(meta, key, labelMap[key] || key, value, { min: 0, max: 42 });
|
||||||
|
});
|
||||||
|
return finalize(questionnaireId, metrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (questionnaireId === 'pss10') {
|
if (questionnaireId === 'pss10') {
|
||||||
const reverse = new Set([4, 5, 7, 8]);
|
const reverse = new Set([4, 5, 7, 8]);
|
||||||
const score = answers.reduce((total, answer, index) => total + (reverse.has(index + 1) ? 4 - Number(answer) : Number(answer)), 0);
|
const score = answers.reduce((total, answer, index) => {
|
||||||
const primary = metric('total', 'PSS-10 总分', score, 40, severity(score, [[0, 'low'], [14, 'moderate'], [27, 'high']]));
|
const value = Number(answer) || 0;
|
||||||
return { primary, metrics: [primary] };
|
return total + (reverse.has(index + 1) ? 4 - value : value);
|
||||||
|
}, 0);
|
||||||
|
const primary = makeMetric(meta, 'total', 'PSS-10 总分', score, { min: 0, max: 40 });
|
||||||
|
return finalize(questionnaireId, [primary], primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (questionnaireId === 'sds') {
|
if (questionnaireId === 'sds') {
|
||||||
const reverse = new Set([2, 5, 6, 11, 12, 14, 16, 17, 18, 20]);
|
const reverse = new Set([2, 5, 6, 11, 12, 14, 16, 17, 18, 20]);
|
||||||
const original = answers.reduce((total, answer, index) => total + (reverse.has(index + 1) ? 5 - Number(answer) : Number(answer)), 0);
|
const original = answers.reduce((total, answer, index) => {
|
||||||
|
const value = Number(answer) || 0;
|
||||||
|
return total + (reverse.has(index + 1) ? 5 - value : value);
|
||||||
|
}, 0);
|
||||||
const score = Math.round(original * 1.25);
|
const score = Math.round(original * 1.25);
|
||||||
const primary = metric('total', 'SDS 标准分', score, 100, severity(score, [[0, 'normal'], [53, 'mild'], [63, 'moderate'], [73, 'severe']]));
|
const primary = makeMetric(meta, 'total', 'SDS 标准分', score, { min: 25, max: 100 });
|
||||||
return { primary, metrics: [primary] };
|
return finalize(questionnaireId, [primary], primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
const primary = metric('raw', '原始作答总和', raw);
|
const raw = sum(answers);
|
||||||
return {
|
const single: Record<string, { label: string; value: number; max?: number }> = {
|
||||||
primary,
|
phq9: { label: 'PHQ-9 总分', value: raw, max: 27 },
|
||||||
metrics: [primary],
|
gad7: { label: 'GAD-7 总分', value: raw, max: 21 },
|
||||||
note: '该数值仅用于保存和核对作答,不替代量表结果页中的正式解释。',
|
isi: { label: 'ISI 总分', value: raw, max: 28 },
|
||||||
|
bdi2: { label: 'BDI-II 总分', value: raw, max: 63 },
|
||||||
|
who5: { label: 'WHO-5 百分制得分', value: raw * 4, max: 100 },
|
||||||
|
crt: { label: 'CRT 答对题数', value: raw, max: 7 },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (single[questionnaireId]) {
|
||||||
|
const item = single[questionnaireId];
|
||||||
|
const primary = makeMetric(meta, 'total', item.label, item.value, { min: meta.min, max: item.max ?? meta.max });
|
||||||
|
return finalize(questionnaireId, [primary], primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
const primary = makeMetric(meta, 'raw', '原始作答总和', raw, {
|
||||||
|
min: meta.min,
|
||||||
|
max: meta.max,
|
||||||
|
direction: meta.direction as ScoreDirection,
|
||||||
|
});
|
||||||
|
return finalize(questionnaireId, [primary], primary, { scoringStatus: 'raw' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,361 @@
|
|||||||
|
import { ScoreDirection, ScoreThreshold } from '@/lib/assessment-types';
|
||||||
|
|
||||||
|
export interface MetricDefinition {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
min: number;
|
||||||
|
max?: number;
|
||||||
|
direction?: ScoreDirection;
|
||||||
|
highScoreMeaning?: string;
|
||||||
|
thresholds?: ScoreThreshold[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuestionnaireScoringMetadata {
|
||||||
|
questionnaireVersion: string;
|
||||||
|
scoreVersion: string;
|
||||||
|
min: number;
|
||||||
|
max?: number;
|
||||||
|
reverseItems: number[];
|
||||||
|
direction: ScoreDirection;
|
||||||
|
highScoreMeaning: string;
|
||||||
|
thresholds: ScoreThreshold[];
|
||||||
|
metrics?: MetricDefinition[];
|
||||||
|
note?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const severityThresholds: ScoreThreshold[] = [
|
||||||
|
{ min: 0, label: '极轻或无' },
|
||||||
|
{ min: 5, label: '轻度' },
|
||||||
|
{ min: 10, label: '中度' },
|
||||||
|
{ min: 15, label: '中重度' },
|
||||||
|
{ min: 20, label: '重度' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const none: ScoreThreshold[] = [];
|
||||||
|
|
||||||
|
export const scoringMetadata: Record<string, QuestionnaireScoringMetadata> = {
|
||||||
|
bigfive: {
|
||||||
|
questionnaireVersion: 'ipip-big-five-50-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-bigfive-50-v1',
|
||||||
|
min: 10,
|
||||||
|
max: 50,
|
||||||
|
reverseItems: [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],
|
||||||
|
direction: 'trait',
|
||||||
|
highScoreMeaning: '高分代表该人格特质更明显,不代表好坏。',
|
||||||
|
thresholds: none,
|
||||||
|
},
|
||||||
|
'bigfive-120': {
|
||||||
|
questionnaireVersion: 'ipip-neo-120-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-ipip-neo-120-v1',
|
||||||
|
min: 24,
|
||||||
|
max: 120,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'trait',
|
||||||
|
highScoreMeaning: '高分代表该人格领域或侧面更明显,不代表好坏。',
|
||||||
|
thresholds: none,
|
||||||
|
},
|
||||||
|
'bigfive-300': {
|
||||||
|
questionnaireVersion: 'ipip-neo-300-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-ipip-neo-300-v1',
|
||||||
|
min: 60,
|
||||||
|
max: 300,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'trait',
|
||||||
|
highScoreMeaning: '高分代表该人格领域或侧面更明显,不代表好坏。',
|
||||||
|
thresholds: none,
|
||||||
|
},
|
||||||
|
'self-esteem': {
|
||||||
|
questionnaireVersion: 'rosenberg-self-esteem-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-rse-v1',
|
||||||
|
min: 0,
|
||||||
|
max: 30,
|
||||||
|
reverseItems: [2, 5, 6, 8, 9],
|
||||||
|
direction: 'protective',
|
||||||
|
highScoreMeaning: '高分通常代表自尊水平更高。',
|
||||||
|
thresholds: [{ min: 0, label: '偏低' }, { min: 15, label: '中等' }, { min: 25, label: '较高' }],
|
||||||
|
},
|
||||||
|
grit: {
|
||||||
|
questionnaireVersion: 'grit-s-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-grit-v1',
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
reverseItems: [1, 3, 5, 6],
|
||||||
|
direction: 'trait',
|
||||||
|
highScoreMeaning: '高分代表坚持与兴趣稳定性更明显。',
|
||||||
|
thresholds: none,
|
||||||
|
},
|
||||||
|
'self-control': {
|
||||||
|
questionnaireVersion: 'brief-self-control-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-self-control-v1',
|
||||||
|
min: 13,
|
||||||
|
max: 65,
|
||||||
|
reverseItems: [1, 2, 3, 4, 5, 7, 9, 10, 12, 13],
|
||||||
|
direction: 'protective',
|
||||||
|
highScoreMeaning: '高分通常代表自我控制能力更强。',
|
||||||
|
thresholds: none,
|
||||||
|
},
|
||||||
|
'need-for-cognition': {
|
||||||
|
questionnaireVersion: 'need-for-cognition-18-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-nfc-v1',
|
||||||
|
min: 18,
|
||||||
|
max: 90,
|
||||||
|
reverseItems: [3, 4, 5, 7, 8, 9, 12, 16, 17],
|
||||||
|
direction: 'trait',
|
||||||
|
highScoreMeaning: '高分代表更倾向主动思考和享受认知活动。',
|
||||||
|
thresholds: none,
|
||||||
|
},
|
||||||
|
maximizer: {
|
||||||
|
questionnaireVersion: 'maximizer-13-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-maximizer-v1',
|
||||||
|
min: 13,
|
||||||
|
max: 65,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'trait',
|
||||||
|
highScoreMeaning: '高分代表决策中最大化倾向更明显。',
|
||||||
|
thresholds: none,
|
||||||
|
},
|
||||||
|
attachment: {
|
||||||
|
questionnaireVersion: 'ecr-rs-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-attachment-v1',
|
||||||
|
min: 1,
|
||||||
|
max: 7,
|
||||||
|
reverseItems: [1, 2, 3, 4, 5, 6],
|
||||||
|
direction: 'mixed',
|
||||||
|
highScoreMeaning: '高分含义取决于维度,焦虑/回避高分通常代表不安全依恋更明显。',
|
||||||
|
thresholds: none,
|
||||||
|
},
|
||||||
|
empathy: {
|
||||||
|
questionnaireVersion: 'iri-28-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-iri-v1',
|
||||||
|
min: 0,
|
||||||
|
max: 28,
|
||||||
|
reverseItems: [3, 4, 7, 12, 13, 14, 15, 18, 19],
|
||||||
|
direction: 'mixed',
|
||||||
|
highScoreMeaning: '高分含义取决于维度,共情维度较高通常代表相关倾向更明显。',
|
||||||
|
thresholds: none,
|
||||||
|
},
|
||||||
|
'dark-triad': {
|
||||||
|
questionnaireVersion: 'short-dark-triad-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-sd3-v1',
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'risk',
|
||||||
|
highScoreMeaning: '高分代表黑暗三联相关人格倾向更明显。',
|
||||||
|
thresholds: none,
|
||||||
|
},
|
||||||
|
hexaco: {
|
||||||
|
questionnaireVersion: 'hexaco-60-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-hexaco-v1',
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'trait',
|
||||||
|
highScoreMeaning: '高分代表该人格维度更明显,不代表好坏。',
|
||||||
|
thresholds: none,
|
||||||
|
},
|
||||||
|
fisher: {
|
||||||
|
questionnaireVersion: 'fisher-temperament-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-fisher-v1',
|
||||||
|
min: 0,
|
||||||
|
max: 40,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'trait',
|
||||||
|
highScoreMeaning: '高分代表对应气质倾向更明显。',
|
||||||
|
thresholds: none,
|
||||||
|
},
|
||||||
|
schwartz: {
|
||||||
|
questionnaireVersion: 'schwartz-values-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-schwartz-v1',
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'trait',
|
||||||
|
highScoreMeaning: '高分代表对应价值观优先级更高。',
|
||||||
|
thresholds: none,
|
||||||
|
},
|
||||||
|
via: {
|
||||||
|
questionnaireVersion: 'via-48-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-via-v1',
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'trait',
|
||||||
|
highScoreMeaning: '高分代表对应性格优势更明显。',
|
||||||
|
thresholds: none,
|
||||||
|
},
|
||||||
|
oeps: {
|
||||||
|
questionnaireVersion: 'open-enneagram-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-oeps-v1',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'trait',
|
||||||
|
highScoreMeaning: '高分代表对应九型人格倾向更明显。',
|
||||||
|
thresholds: none,
|
||||||
|
},
|
||||||
|
crt: {
|
||||||
|
questionnaireVersion: 'crt-7-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-crt-v1',
|
||||||
|
min: 0,
|
||||||
|
max: 7,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'ability',
|
||||||
|
highScoreMeaning: '高分代表答对题数更多,分析性反思表现更好。',
|
||||||
|
thresholds: [{ min: 0, label: '较低' }, { min: 3, label: '中等' }, { min: 6, label: '较高' }],
|
||||||
|
},
|
||||||
|
riasec: {
|
||||||
|
questionnaireVersion: 'riasec-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-riasec-v1',
|
||||||
|
min: 0,
|
||||||
|
max: 40,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'trait',
|
||||||
|
highScoreMeaning: '高分代表对应职业兴趣类型更明显。',
|
||||||
|
thresholds: none,
|
||||||
|
},
|
||||||
|
'career-anchors': {
|
||||||
|
questionnaireVersion: 'career-anchors-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-career-anchors-v1',
|
||||||
|
min: 1,
|
||||||
|
max: 6,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'trait',
|
||||||
|
highScoreMeaning: '高分代表对应职业锚更重要。',
|
||||||
|
thresholds: none,
|
||||||
|
},
|
||||||
|
dass21: {
|
||||||
|
questionnaireVersion: 'dass-21-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-dass21-v1',
|
||||||
|
min: 0,
|
||||||
|
max: 42,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'risk',
|
||||||
|
highScoreMeaning: '高分代表抑郁、焦虑或压力相关困扰更明显。',
|
||||||
|
thresholds: [{ min: 0, label: '正常' }, { min: 10, label: '轻度' }, { min: 14, label: '中度' }, { min: 21, label: '重度' }, { min: 28, label: '极重度' }],
|
||||||
|
},
|
||||||
|
who5: {
|
||||||
|
questionnaireVersion: 'who-5-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-who5-v1',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'protective',
|
||||||
|
highScoreMeaning: '高分代表主观幸福感更高。',
|
||||||
|
thresholds: [{ min: 0, label: '需要关注' }, { min: 50, label: '一般或较好' }],
|
||||||
|
},
|
||||||
|
ocd: {
|
||||||
|
questionnaireVersion: 'ybocs-self-report-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-ocd-v1',
|
||||||
|
min: 0,
|
||||||
|
max: 40,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'risk',
|
||||||
|
highScoreMeaning: '高分代表强迫相关困扰更明显。',
|
||||||
|
thresholds: [{ min: 0, label: '亚临床' }, { min: 8, label: '轻度' }, { min: 16, label: '中度' }, { min: 24, label: '重度' }, { min: 32, label: '极重度' }],
|
||||||
|
},
|
||||||
|
scl90: {
|
||||||
|
questionnaireVersion: 'scl-90-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-scl90-v1',
|
||||||
|
min: 90,
|
||||||
|
max: 450,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'risk',
|
||||||
|
highScoreMeaning: '高分代表近期心理症状负担更高。',
|
||||||
|
thresholds: [{ min: 90, label: '较低' }, { min: 160, label: '需关注' }, { min: 200, label: '较高' }],
|
||||||
|
},
|
||||||
|
sds: {
|
||||||
|
questionnaireVersion: 'zung-sds-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-sds-v1',
|
||||||
|
min: 25,
|
||||||
|
max: 100,
|
||||||
|
reverseItems: [2, 5, 6, 11, 12, 14, 16, 17, 18, 20],
|
||||||
|
direction: 'risk',
|
||||||
|
highScoreMeaning: '高分代表抑郁相关症状更明显。',
|
||||||
|
thresholds: [{ min: 0, label: '正常范围' }, { min: 53, label: '轻度' }, { min: 63, label: '中度' }, { min: 73, label: '重度' }],
|
||||||
|
},
|
||||||
|
gad7: {
|
||||||
|
questionnaireVersion: 'gad-7-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-gad7-v1',
|
||||||
|
min: 0,
|
||||||
|
max: 21,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'risk',
|
||||||
|
highScoreMeaning: '高分代表焦虑相关症状更明显。',
|
||||||
|
thresholds: [{ min: 0, label: '极轻或无' }, { min: 5, label: '轻度' }, { min: 10, label: '中度' }, { min: 15, label: '重度' }],
|
||||||
|
},
|
||||||
|
phq9: {
|
||||||
|
questionnaireVersion: 'phq-9-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-phq9-v1',
|
||||||
|
min: 0,
|
||||||
|
max: 27,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'risk',
|
||||||
|
highScoreMeaning: '高分代表抑郁相关症状更明显。',
|
||||||
|
thresholds: severityThresholds,
|
||||||
|
},
|
||||||
|
pss10: {
|
||||||
|
questionnaireVersion: 'pss-10-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-pss10-v1',
|
||||||
|
min: 0,
|
||||||
|
max: 40,
|
||||||
|
reverseItems: [4, 5, 7, 8],
|
||||||
|
direction: 'risk',
|
||||||
|
highScoreMeaning: '高分代表主观压力更高。',
|
||||||
|
thresholds: [{ min: 0, label: '较低' }, { min: 14, label: '中等' }, { min: 27, label: '较高' }],
|
||||||
|
},
|
||||||
|
bdi2: {
|
||||||
|
questionnaireVersion: 'bdi-ii-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-bdi2-v1',
|
||||||
|
min: 0,
|
||||||
|
max: 63,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'risk',
|
||||||
|
highScoreMeaning: '高分代表抑郁相关症状更明显。',
|
||||||
|
thresholds: [{ min: 0, label: '极轻或无' }, { min: 14, label: '轻度' }, { min: 20, label: '中度' }, { min: 29, label: '重度' }],
|
||||||
|
},
|
||||||
|
isi: {
|
||||||
|
questionnaireVersion: 'isi-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-isi-v1',
|
||||||
|
min: 0,
|
||||||
|
max: 28,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'risk',
|
||||||
|
highScoreMeaning: '高分代表失眠困扰更明显。',
|
||||||
|
thresholds: [{ min: 0, label: '无明显失眠' }, { min: 8, label: '亚阈值' }, { min: 15, label: '中度' }, { min: 22, label: '重度' }],
|
||||||
|
},
|
||||||
|
adhd: {
|
||||||
|
questionnaireVersion: 'asrs-v1-1-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-adhd-v1',
|
||||||
|
min: 0,
|
||||||
|
max: 72,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'risk',
|
||||||
|
highScoreMeaning: '高分代表注意力、冲动或执行功能相关困扰更明显。',
|
||||||
|
thresholds: [{ min: 0, label: '较低' }, { min: 24, label: '需关注' }],
|
||||||
|
},
|
||||||
|
gd: {
|
||||||
|
questionnaireVersion: 'gaming-disorder-questionnaire-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-gd-v1',
|
||||||
|
min: 0,
|
||||||
|
max: 36,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'risk',
|
||||||
|
highScoreMeaning: '高分代表游戏相关困扰更明显。',
|
||||||
|
thresholds: [{ min: 0, label: '较低' }, { min: 20, label: '需关注' }],
|
||||||
|
},
|
||||||
|
npd: {
|
||||||
|
questionnaireVersion: 'npi-16-zh-v1',
|
||||||
|
scoreVersion: 'mindscope-npi16-v1',
|
||||||
|
min: 0,
|
||||||
|
max: 16,
|
||||||
|
reverseItems: [],
|
||||||
|
direction: 'trait',
|
||||||
|
highScoreMeaning: '高分代表自恋相关人格倾向更明显。',
|
||||||
|
thresholds: none,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getScoringMetadata(questionnaireId: string) {
|
||||||
|
return scoringMetadata[questionnaireId];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user