feat: 完善中文心理测评平台
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { Questionnaire } from '@/types';
|
||||
import { useScopedI18n } from '@/locales/client';
|
||||
|
||||
interface AnswerListProps {
|
||||
questions: Questionnaire['questions'];
|
||||
answers: string[]; // array of selected option values in order
|
||||
renderOptions: (id: number) => { id: number; content: string; value: string }[];
|
||||
}
|
||||
|
||||
export function AnswerList({ questions, answers, renderOptions }: AnswerListProps) {
|
||||
const t = useScopedI18n('common');
|
||||
|
||||
if (!questions || questions.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-medium mb-3">{t('answerList.title')}</h3>
|
||||
<div className="space-y-2">
|
||||
{questions.map((q, idx) => {
|
||||
const selectedValue = answers[idx];
|
||||
|
||||
const optionContent = selectedValue !== undefined ? (() => {
|
||||
const opts = renderOptions(q.id) || [];
|
||||
const found = opts.find(o => String(o.value) === String(selectedValue));
|
||||
return found ? found.content : `${t('answerList.option')} ${selectedValue}`;
|
||||
})() : t('answerList.unanswered');
|
||||
|
||||
return (
|
||||
<div
|
||||
key={q.id}
|
||||
className="flex items-start gap-2 p-3 bg-gray-50 rounded-md text-sm"
|
||||
>
|
||||
<span className="font-medium">{idx + 1}. {q.content}</span>
|
||||
<span className="ml-auto">
|
||||
{selectedValue !== undefined ? `${optionContent}` : t('answerList.unanswered')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import Link from 'next/link';
|
||||
import { ReactNode } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useScopedI18n } from '@/locales/client';
|
||||
import { Copy, Download, FileText } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Questionnaire } from '@/types';
|
||||
|
||||
interface ResultContainerProps {
|
||||
title: string;
|
||||
id: string;
|
||||
children: ReactNode;
|
||||
questionnaire?: Questionnaire;
|
||||
answers?: string[];
|
||||
questionnaireResults?: Record<string, string>;
|
||||
}
|
||||
|
||||
export function ResultContainer({ title, id, children, questionnaire, answers, questionnaireResults }: ResultContainerProps) {
|
||||
const t = useScopedI18n(
|
||||
'component.questionnaire.result.public.resultContainer'
|
||||
);
|
||||
|
||||
const handleCopyResultLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(
|
||||
`${window.location.origin}${window.location.pathname}`,
|
||||
);
|
||||
toast.success(t('copySuccess'));
|
||||
} catch {
|
||||
toast.error(t('copyError'));
|
||||
}
|
||||
};
|
||||
|
||||
const buildResultMarkdown = () => {
|
||||
if (!questionnaire || !answers || !questionnaireResults) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentTime = new Date().toLocaleString();
|
||||
|
||||
let resultData = `# ${t('copyTemplate.title')}\n\n`;
|
||||
resultData += `## ${t('copyTemplate.basicInfo')}\n`;
|
||||
resultData += `- ${t('copyTemplate.questionnaireName')}: ${questionnaire.title}\n`;
|
||||
resultData += `- ${t('copyTemplate.questionnaireId')}: ${id}\n`;
|
||||
resultData += `- ${t('copyTemplate.assessmentTime')}: ${currentTime}\n`;
|
||||
resultData += `- ${t('copyTemplate.questionCount')}: ${questionnaire.questions.length}\n\n`;
|
||||
|
||||
resultData += `## ${t('copyTemplate.questionsAndAnswers')}\n`;
|
||||
Object.entries(questionnaireResults).forEach(([question, answer], index) => {
|
||||
resultData += `${index + 1}. ${question}\n ${t('copyTemplate.answer')}: ${answer}\n\n`;
|
||||
});
|
||||
|
||||
resultData += `## ${t('copyTemplate.usage')}\n`;
|
||||
resultData += `${t('copyTemplate.disclaimer')}\n\n`;
|
||||
resultData += `${t('copyTemplate.source')}: ${t('copyTemplate.platform')}\n`;
|
||||
resultData += `${t('copyTemplate.website')}: ${window.location.origin}\n`;
|
||||
|
||||
return resultData;
|
||||
};
|
||||
|
||||
const handleCopyResultData = async () => {
|
||||
const resultData = buildResultMarkdown();
|
||||
if (!resultData) {
|
||||
toast.error(t('copyResultDataError'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(resultData);
|
||||
toast.success(t('copyResultDataSuccess'));
|
||||
} catch {
|
||||
toast.error(t('copyResultDataError'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadResultData = () => {
|
||||
const resultData = buildResultMarkdown();
|
||||
if (!resultData) {
|
||||
toast.error(t('downloadResultDataError'));
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([resultData], { type: 'text/markdown;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = `${id}-result.md`;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success(t('downloadResultDataSuccess'));
|
||||
};
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen md:p-4 p-2">
|
||||
<div className="max-w-6xl w-full bg-white rounded-lg shadow-lg md:p-8 p-4 border">
|
||||
<h1 className="text-2xl font-bold mb-6">
|
||||
{title} - {t('resultText')}
|
||||
</h1>
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="space-y-6">{children}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between gap-4 mt-8">
|
||||
<Button variant="outline" className="w-full sm:w-auto">
|
||||
<Link href={`/questionnaire/${id}`}>{t('backToDetail')}</Link>
|
||||
</Button>
|
||||
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
|
||||
<Button variant="outline" onClick={handleCopyResultLink} className="w-full sm:w-auto">
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
{t('copyResultLink')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCopyResultData}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
{t('copyResultData')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleDownloadResultData}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
{t('downloadResultData')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user