feat: 增加本地测评档案与长期追踪

This commit is contained in:
2026-06-23 00:21:07 +02:00
parent fdfbfa063f
commit c8d5a918cf
19 changed files with 1509 additions and 14 deletions
@@ -5,6 +5,8 @@ import { useScopedI18n } from '@/locales/client';
import { Copy, Download, FileText } from 'lucide-react';
import { toast } from 'sonner';
import { Questionnaire } from '@/types';
import { AssessmentRecord } from '@/lib/assessment-types';
import { recordToMarkdown } from '@/lib/assessment-export';
interface ResultContainerProps {
title: string;
@@ -13,9 +15,11 @@ interface ResultContainerProps {
questionnaire?: Questionnaire;
answers?: string[];
questionnaireResults?: Record<string, string>;
record?: AssessmentRecord;
profileName?: string;
}
export function ResultContainer({ title, id, children, questionnaire, answers, questionnaireResults }: ResultContainerProps) {
export function ResultContainer({ title, id, children, questionnaire, answers, questionnaireResults, record, profileName = '未命名档案' }: ResultContainerProps) {
const t = useScopedI18n(
'component.questionnaire.result.public.resultContainer'
);
@@ -32,6 +36,9 @@ export function ResultContainer({ title, id, children, questionnaire, answers, q
};
const buildResultMarkdown = () => {
if (record) {
return recordToMarkdown(record, profileName);
}
if (!questionnaire || !answers || !questionnaireResults) {
return null;
}
@@ -125,6 +132,9 @@ export function ResultContainer({ title, id, children, questionnaire, answers, q
<Download className="w-4 h-4 mr-2" />
{t('downloadResultData')}
</Button>
<Button asChild className="w-full sm:w-auto">
<Link href="/records"></Link>
</Button>
</div>
</div>
</div>
@@ -10,6 +10,10 @@ import { saveResult } from '@/lib/result-storage';
import { Questionnaire as QuestionnaireType, QuestionType } from '@/types';
import { useRouter } from 'next/navigation';
import { toast } from "sonner"
import { ProfilePicker } from '@/components/records/ProfilePicker';
import { AssessmentProfile } from '@/lib/assessment-types';
import { addAssessmentRecord, ensureActiveProfile } from '@/lib/assessment-db';
import { buildScoreSummary } from '@/lib/score-summary';
interface QuestionnaireProps {
questionnaire: QuestionnaireType;
@@ -32,6 +36,7 @@ export function Questionnaire({
const questionRefs = useRef<{ [key: number]: HTMLDivElement | null }>({});
// Flag to indicate whether the questionnaire has been submitted
const hasSubmittedRef = useRef(false);
const [activeProfile, setActiveProfile] = useState<AssessmentProfile | null>(null);
// Save answers when component unmounts
useEffect(() => {
@@ -163,7 +168,7 @@ export function Questionnaire({
}
};
const handleSubmit = () => {
const handleSubmit = async () => {
// Check if all questions are answered first
if (answeredCount < questions.length) {
toast("请先完成所有题目");
@@ -179,7 +184,32 @@ export function Questionnaire({
// Store answers in this browser tab instead of putting them in the URL.
const resultAnswers = questions.map((q) => answers[q.id] ?? '0');
saveResult(id, resultAnswers);
const profile = activeProfile || await ensureActiveProfile();
const completedAt = new Date().toISOString();
const recordedAnswers = questionnaire.questions.map((question, index) => {
const value = resultAnswers[index];
const option = questionnaire.renderOptions(question.id).find(
(item) => String(item.value) === String(value),
);
return {
questionId: question.id,
question: question.content,
value,
answer: option?.content || value,
};
});
const record = await addAssessmentRecord({
profileId: profile.id,
questionnaireId: id,
questionnaireTitle: questionnaire.title,
category: questionnaire.category,
completedAt,
answers: recordedAnswers,
scoreSummary: buildScoreSummary(id, resultAnswers),
retestSuitable: questionnaire.evaluation?.retestSuitable,
recommendedInterval: questionnaire.evaluation?.recommendedInterval,
});
saveResult(id, resultAnswers, profile.id, record.id);
router.push(`/questionnaire/${id}/result`);
}
@@ -201,6 +231,8 @@ export function Questionnaire({
<div className="max-w-3xl mx-auto py-8 px-4">
<h1 className="text-2xl font-bold mb-8">{questionnaire.title}</h1>
<ProfilePicker onChange={setActiveProfile} />
<ProgressPanel
questions={questions}
answers={answers}