dramaling-vocab-learning/frontend/components/review/NavigationController.tsx

242 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { memo, useCallback, useMemo } from 'react'
import { useTestQueueStore } from '@/store/useTestQueueStore'
/**
* 智能導航控制器
* 根據PRD US-008要求實現
* - 答題前狀態:只顯示「跳過」按鈕
* - 答題後狀態:只顯示「繼續」按鈕
* - 答題提交與導航按鈕完全分離
*/
export interface NavigationState {
status: 'unanswered' | 'answered' | 'skipped'
canSkip: boolean
canContinue: boolean
isLastTest: boolean
hasAnswered: boolean
}
interface NavigationControllerProps {
// 當前測驗狀態
hasAnswered: boolean
isLastTest?: boolean
// 導航動作
onSkip: () => void
onContinue: () => void
// 樣式配置
className?: string
buttonClassName?: string
// 禁用狀態
disabled?: boolean
}
export const NavigationController: React.FC<NavigationControllerProps> = memo(({
hasAnswered,
isLastTest = false,
onSkip,
onContinue,
className = '',
buttonClassName = '',
disabled = false
}) => {
// 計算導航狀態
const navigationState: NavigationState = useMemo(() => ({
status: hasAnswered ? 'answered' : 'unanswered',
canSkip: !hasAnswered && !disabled,
canContinue: hasAnswered && !disabled,
isLastTest,
hasAnswered
}), [hasAnswered, isLastTest, disabled])
// 處理跳過動作
const handleSkip = useCallback(() => {
if (navigationState.canSkip) {
onSkip()
}
}, [navigationState.canSkip, onSkip])
// 處理繼續動作
const handleContinue = useCallback(() => {
if (navigationState.canContinue) {
onContinue()
}
}, [navigationState.canContinue, onContinue])
// 獲取按鈕文字
const getButtonText = () => {
if (navigationState.status === 'answered') {
return isLastTest ? '完成學習' : '繼續'
}
return '跳過'
}
// 獲取按鈕樣式
const getButtonStyles = () => {
const baseStyles = `px-6 py-3 rounded-lg font-medium transition-all ${buttonClassName}`
if (navigationState.status === 'answered') {
// 答題後 - 繼續按鈕(主要動作)
return `${baseStyles} bg-blue-600 text-white hover:bg-blue-700 ${
disabled ? 'opacity-50 cursor-not-allowed' : 'shadow-lg hover:shadow-xl'
}`
} else {
// 答題前 - 跳過按鈕(次要動作)
return `${baseStyles} bg-gray-200 text-gray-700 hover:bg-gray-300 ${
disabled ? 'opacity-50 cursor-not-allowed' : ''
}`
}
}
// 獲取按鈕圖示
const getButtonIcon = () => {
if (navigationState.status === 'answered') {
return isLastTest ? '🎉' : '➡️'
}
return '⏭️'
}
return (
<div className={`navigation-controller ${className}`}>
{/* 狀態驅動的單一按鈕 */}
<div className="flex justify-center">
<button
onClick={navigationState.status === 'answered' ? handleContinue : handleSkip}
disabled={disabled}
className={getButtonStyles()}
aria-label={getButtonText()}
>
<span className="flex items-center gap-2">
<span>{getButtonIcon()}</span>
<span>{getButtonText()}</span>
</span>
</button>
</div>
{/* 狀態提示(開發模式可見) */}
{process.env.NODE_ENV === 'development' && (
<div className="mt-2 text-xs text-gray-500 text-center">
: {navigationState.status} |
: {navigationState.canSkip ? '是' : '否'} |
: {navigationState.canContinue ? '是' : '否'}
</div>
)}
</div>
)
})
NavigationController.displayName = 'NavigationController'
/**
* 整合測試隊列的高階導航控制器
* 自動處理隊列狀態更新和測驗導航
*/
interface SmartNavigationControllerProps {
hasAnswered: boolean
className?: string
buttonClassName?: string
disabled?: boolean
onSkipCallback?: () => void
onContinueCallback?: () => void
}
export const SmartNavigationController: React.FC<SmartNavigationControllerProps> = memo(({
hasAnswered,
className,
buttonClassName,
disabled,
onSkipCallback,
onContinueCallback
}) => {
const {
currentTestIndex,
testItems,
skipCurrentTest,
goToNextTest
} = useTestQueueStore()
// 判斷是否為最後一個測驗
const isLastTest = useMemo(() => {
const remainingTests = testItems.filter(item => !item.isCompleted)
return remainingTests.length <= 1
}, [testItems])
// 處理跳過
const handleSkip = useCallback(() => {
skipCurrentTest()
onSkipCallback?.()
}, [skipCurrentTest, onSkipCallback])
// 處理繼續
const handleContinue = useCallback(() => {
goToNextTest()
onContinueCallback?.()
}, [goToNextTest, onContinueCallback])
return (
<NavigationController
hasAnswered={hasAnswered}
isLastTest={isLastTest}
onSkip={handleSkip}
onContinue={handleContinue}
className={className}
buttonClassName={buttonClassName}
disabled={disabled}
/>
)
})
SmartNavigationController.displayName = 'SmartNavigationController'
/**
* 導航狀態判斷工具函數
*/
export const getNavigationState = (
hasAnswered: boolean,
isSkipped: boolean = false,
disabled: boolean = false
): NavigationState => {
let status: NavigationState['status'] = 'unanswered'
if (isSkipped) {
status = 'skipped'
} else if (hasAnswered) {
status = 'answered'
}
return {
status,
canSkip: !hasAnswered && !isSkipped && !disabled,
canContinue: hasAnswered && !disabled,
isLastTest: false,
hasAnswered
}
}
/**
* 導航控制器的設定選項
*/
export interface NavigationConfig {
showSkipButton: boolean
showContinueButton: boolean
skipButtonText: string
continueButtonText: string
completeButtonText: string
skipButtonIcon: string
continueButtonIcon: string
completeButtonIcon: string
}
export const defaultNavigationConfig: NavigationConfig = {
showSkipButton: true,
showContinueButton: true,
skipButtonText: '跳過',
continueButtonText: '繼續',
completeButtonText: '完成學習',
skipButtonIcon: '⏭️',
continueButtonIcon: '➡️',
completeButtonIcon: '🎉'
}