126 lines
4.0 KiB
TypeScript
126 lines
4.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import {
|
|
ChevronDown,
|
|
ListChecks,
|
|
PanelRightClose,
|
|
PanelRightOpen,
|
|
} from 'lucide-react';
|
|
import { useScopedI18n } from '@/locales/client';
|
|
import { Option } from '@/types';
|
|
|
|
interface Question {
|
|
id: number;
|
|
content: string;
|
|
options: Option[];
|
|
}
|
|
|
|
interface ProgressPanelProps {
|
|
questions: Question[];
|
|
answers: { [key: number]: string };
|
|
activePanelQuestion: number | null;
|
|
goToQuestion: (questionId: number) => void;
|
|
showProgressPanel: boolean;
|
|
toggleProgressPanel: () => void;
|
|
completionPercentage: number;
|
|
}
|
|
|
|
export function ProgressPanel({
|
|
questions,
|
|
answers,
|
|
activePanelQuestion,
|
|
goToQuestion,
|
|
showProgressPanel,
|
|
toggleProgressPanel,
|
|
completionPercentage,
|
|
}: ProgressPanelProps) {
|
|
const t = useScopedI18n('component.questionnaire.test.public.progressPanel');
|
|
const [showMobileQuestions, setShowMobileQuestions] = useState(false);
|
|
|
|
const questionGrid = (
|
|
<div className="grid grid-cols-5 gap-2 overflow-y-auto sm:grid-cols-8 lg:mb-4 lg:max-h-[400px] lg:grid-cols-5 lg:gap-1">
|
|
{questions.map((_, index) => {
|
|
const questionNumber = index + 1;
|
|
return (
|
|
<button
|
|
type="button"
|
|
key={questionNumber}
|
|
className={`flex h-11 min-w-11 items-center justify-center rounded-md text-sm lg:h-9 lg:w-9 lg:min-w-0 lg:text-xs
|
|
${answers[questionNumber] ? 'border-2 border-green-600 bg-green-50' : 'border bg-white'}
|
|
${questionNumber === activePanelQuestion ? 'border-green-700 bg-green-200' : ''}
|
|
active:bg-gray-100 lg:hover:bg-gray-100`}
|
|
onClick={() => {
|
|
goToQuestion(questionNumber);
|
|
setShowMobileQuestions(false);
|
|
}}
|
|
title={t('jumpToQuestion', { questionNumber })}
|
|
>
|
|
{questionNumber}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<div className="mb-4 lg:hidden">
|
|
<button
|
|
type="button"
|
|
className="flex min-h-12 w-full items-center justify-between rounded-md border bg-muted/30 px-4 text-base"
|
|
aria-expanded={showMobileQuestions}
|
|
onClick={() => setShowMobileQuestions((current) => !current)}
|
|
>
|
|
<span className="flex items-center gap-2">
|
|
<ListChecks className="h-5 w-5" />
|
|
题目进度 {Object.keys(answers).length} / {questions.length}
|
|
</span>
|
|
<ChevronDown
|
|
className={`h-5 w-5 transition-transform ${showMobileQuestions ? 'rotate-180' : ''}`}
|
|
/>
|
|
</button>
|
|
{showMobileQuestions && (
|
|
<div className="mt-3 border bg-muted/20 p-3">{questionGrid}</div>
|
|
)}
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
className="fixed right-4 top-16 z-40 hidden h-11 w-11 items-center justify-center rounded-full bg-blue-600 text-white transition-colors hover:bg-blue-700 lg:flex"
|
|
onClick={toggleProgressPanel}
|
|
aria-label={showProgressPanel ? t('hideNav') : t('showNav')}
|
|
title={showProgressPanel ? t('hideNav') : t('showNav')}
|
|
>
|
|
{showProgressPanel ? (
|
|
<PanelRightClose className="h-5 w-5" />
|
|
) : (
|
|
<PanelRightOpen className="h-5 w-5" />
|
|
)}
|
|
</button>
|
|
|
|
<div
|
|
className={`fixed right-4 top-28 z-30 hidden w-64 rounded-lg border bg-white p-4 shadow-lg transition-transform duration-300 lg:block ${
|
|
showProgressPanel
|
|
? 'translate-x-0'
|
|
: 'translate-x-[calc(100%+2rem)]'
|
|
}`}
|
|
>
|
|
<div className="mb-4">
|
|
<div className="mb-2 text-sm font-medium">
|
|
{t('completionProgress')}
|
|
{Math.round(completionPercentage)}%
|
|
</div>
|
|
<div className="h-2 w-full rounded-full bg-gray-200">
|
|
<div
|
|
className="h-2 rounded-full bg-blue-500 transition-all duration-300"
|
|
style={{ width: `${completionPercentage}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{questionGrid}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|