feat: 发布 v0.3.0 移动端答题优化

This commit is contained in:
2026-06-23 00:51:32 +02:00
parent 89675a526f
commit 81a70137a9
11 changed files with 134 additions and 71 deletions
@@ -19,23 +19,25 @@ export function Navigation({
}: NavigationProps) {
const t = useScopedI18n('component.questionnaire.test.public.navigation');
return (
<div className="flex justify-between items-center mt-8">
<div className="fixed inset-x-0 bottom-0 z-30 flex items-center justify-between gap-3 border-t bg-background/95 px-4 pt-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))] shadow-[0_-4px_16px_rgba(0,0,0,0.06)] backdrop-blur lg:static lg:mt-8 lg:border-0 lg:bg-transparent lg:p-0 lg:shadow-none lg:backdrop-blur-none">
<Button
variant="outline"
className="h-11 min-w-24 text-base lg:h-9 lg:min-w-0 lg:text-sm"
onClick={() => goToPage(currentPage - 1)}
disabled={currentPage === 1}
>
{t('previousPage')}
</Button>
<span className="text-sm text-gray-500">
<span className="shrink-0 text-xs text-gray-500 sm:text-sm">
{t('pageInfo', { currentPage, totalPages })}
</span>
{isLastPage ? (
<Button onClick={onSubmit}>{t('submit')}</Button>
<Button className="h-11 min-w-24 text-base lg:h-9 lg:min-w-0 lg:text-sm" onClick={onSubmit}>{t('submit')}</Button>
) : (
<Button
className="h-11 min-w-24 text-base lg:h-9 lg:min-w-0 lg:text-sm"
onClick={() => goToPage(currentPage + 1)}
disabled={currentPage === totalPages}
>
@@ -6,7 +6,7 @@ interface ProgressBarProps {
export function ProgressBar({ completionPercentage }: ProgressBarProps) {
return (
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden mb-2">
<div className="mb-4 h-1.5 w-full overflow-hidden rounded-full bg-gray-200 md:mb-2 md:h-2">
<div
className="h-full bg-green-500 transition-all duration-300"
style={{ width: `${completionPercentage}%` }}
@@ -1,5 +1,15 @@
'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;
@@ -26,68 +36,89 @@ export function ProgressPanel({
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
className="fixed right-4 top-10 bg-blue-500 hover:bg-blue-600 text-white rounded-full p-2 z-40 transition-all duration-300"
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')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className={`h-6 w-6 transition-transform duration-300 ${showProgressPanel ? 'rotate-180' : ''
}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
{showProgressPanel ? (
<PanelRightClose className="h-5 w-5" />
) : (
<PanelRightOpen className="h-5 w-5" />
)}
</button>
<div
className={`fixed right-4 top-20 w-64 bg-white rounded-lg shadow-lg p-4 transition-transform duration-300 transform ${showProgressPanel ? 'translate-x-0' : 'translate-x-full'
}`}
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="text-sm font-medium mb-2">
<div className="mb-2 text-sm font-medium">
{t('completionProgress')}
{Math.round(completionPercentage)}%
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div className="h-2 w-full rounded-full bg-gray-200">
<div
className="bg-blue-500 rounded-full h-2 transition-all duration-300"
className="h-2 rounded-full bg-blue-500 transition-all duration-300"
style={{ width: `${completionPercentage}%` }}
/>
</div>
</div>
<div className="grid grid-cols-5 gap-1 mb-4 max-h-[400px] overflow-y-auto">
{questions.map((_, i) => (
<button
key={i}
className={`w-9 h-9 flex items-center justify-center rounded-md text-xs
${answers[i + 1]
? 'bg-green-100 border-green-500 border-2'
: 'border'
}
${i + 1 === activePanelQuestion
? 'bg-green-300 border-green-600'
: ''
}
hover:bg-gray-100 transition-colors duration-200`}
onClick={() => goToQuestion(i + 1)}
title={t('jumpToQuestion', { questionNumber: i + 1 })}
>
{i + 1}
</button>
))}
</div>
{questionGrid}
</div>
</>
);
@@ -12,20 +12,22 @@ export const Question = forwardRef<HTMLDivElement, QuestionProps>(
return (
<div
ref={ref}
className="mb-8 p-6 bg-white rounded-lg shadow-sm"
className="mb-4 scroll-mt-20 rounded-md border bg-white p-4 shadow-sm md:mb-8 md:p-6"
id={`question-${question.id}`}
>
<h3 className="text-lg font-medium mb-4">
<h3 className="mb-4 text-[17px] font-medium leading-[1.65] md:text-lg md:leading-7">
{question.id}. {question.content}
</h3>
<div className="space-y-2">
{question.options.map((option) => (
<button
type="button"
aria-pressed={answer === option.value}
key={option.value}
className={`w-full text-left p-3 rounded-lg transition-colors duration-200
className={`min-h-12 w-full touch-manipulation rounded-md px-4 py-3 text-left text-base leading-6 transition-colors duration-200
${answer === option.value
? 'bg-blue-100 border-blue-500 border-2'
: 'border hover:bg-gray-50'
? 'border-2 border-blue-600 bg-blue-50 font-medium text-blue-950'
: 'border bg-white active:bg-gray-100 md:hover:bg-gray-50'
}`}
onClick={() => onSelect(question.id, option.value)}
>
@@ -38,4 +40,4 @@ export const Question = forwardRef<HTMLDivElement, QuestionProps>(
}
);
Question.displayName = 'Question';
Question.displayName = 'Question';