242 lines
6.3 KiB
TypeScript
242 lines
6.3 KiB
TypeScript
import React, { memo, useCallback, useMemo } from 'react'
|
||
import { useTestQueueStore } from '@/store/review/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: '🎉'
|
||
} |