dramaling-vocab-learning/frontend/components/SegmentedProgressBar.tsx

166 lines
5.4 KiB
TypeScript

'use client'
import { useState } from 'react'
interface CardSegment {
cardId: string
word: string
plannedTests: number
completedTests: number
isCompleted: boolean
widthPercentage: number
position: number
}
interface SegmentedProgressBarProps {
progress: {
cards: Array<{
cardId: string
word: string
plannedTests: string[]
completedTestsCount: number
isCompleted: boolean
}>
totalTests: number
completedTests: number
}
onClick?: () => void
}
export default function SegmentedProgressBar({ progress, onClick }: SegmentedProgressBarProps) {
const [hoveredWord, setHoveredWord] = useState<string | null>(null)
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 })
// 計算每個詞卡的分段數據
const segments: CardSegment[] = progress.cards.map((card, index) => {
const plannedTests = card.plannedTests.length
const completedTests = card.completedTestsCount
const widthPercentage = (plannedTests / progress.totalTests) * 100
// 計算位置(累積前面所有詞卡的寬度)
const position = progress.cards
.slice(0, index)
.reduce((acc, prevCard) => acc + (prevCard.plannedTests.length / progress.totalTests) * 100, 0)
return {
cardId: card.cardId,
word: card.word,
plannedTests,
completedTests,
isCompleted: card.isCompleted,
widthPercentage,
position
}
})
const handleMouseMove = (event: React.MouseEvent, word: string) => {
setHoveredWord(word)
setTooltipPosition({ x: event.clientX, y: event.clientY })
}
const handleMouseLeave = () => {
setHoveredWord(null)
}
return (
<div className="relative">
{/* 分段式進度條 */}
<div
className="w-full bg-gray-200 rounded-full h-4 cursor-pointer hover:bg-gray-300 transition-colors relative overflow-hidden"
onClick={onClick}
title="點擊查看詳細進度"
>
{segments.map((segment, index) => {
// 計算當前段落的完成比例
const segmentProgress = segment.plannedTests > 0 ? segment.completedTests / segment.plannedTests : 0
return (
<div
key={segment.cardId}
className="absolute top-0 h-full flex"
style={{
left: `${segment.position}%`,
width: `${segment.widthPercentage}%`
}}
>
{/* 背景(未完成部分) */}
<div className="w-full h-full bg-gray-300 rounded-sm" />
{/* 已完成部分 */}
<div
className={`absolute top-0 left-0 h-full rounded-sm transition-all duration-300 ${
segment.isCompleted
? 'bg-green-500'
: 'bg-blue-500'
}`}
style={{ width: `${segmentProgress * 100}%` }}
/>
{/* 分界線(右邊界) */}
{index < segments.length - 1 && (
<div className="absolute top-0 right-0 w-px h-full bg-white opacity-60" />
)}
</div>
)
})}
</div>
{/* 詞卡標誌點 */}
<div className="relative w-full h-0">
{segments.map((segment, index) => {
// 標誌點位置(在每個詞卡段落的中心)
const markerPosition = segment.position + (segment.widthPercentage / 2)
return (
<div
key={`marker-${segment.cardId}`}
className="absolute transform -translate-x-1/2"
style={{
left: `${markerPosition}%`,
top: '-2px'
}}
>
<div
className={`w-3 h-3 rounded-full border-2 border-white shadow-sm cursor-pointer transition-all hover:scale-125 ${
segment.isCompleted
? 'bg-green-500'
: segment.completedTests > 0
? 'bg-blue-500'
: 'bg-gray-400'
}`}
onMouseMove={(e) => handleMouseMove(e, segment.word)}
onMouseLeave={handleMouseLeave}
title={segment.word}
/>
</div>
)
})}
</div>
{/* Tooltip */}
{hoveredWord && (
<div
className="fixed z-50 bg-gray-900 text-white px-3 py-2 rounded-lg text-sm font-medium pointer-events-none shadow-lg"
style={{
left: tooltipPosition.x + 10,
top: tooltipPosition.y - 35
}}
>
{hoveredWord}
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-l-transparent border-r-transparent border-t-gray-900" />
</div>
)}
{/* 進度統計 */}
<div className="mt-3 flex justify-between items-center text-xs text-gray-600">
<span>
: {progress.cards.filter(c => c.isCompleted).length} / {progress.cards.length}
</span>
<span>
: {progress.completedTests} / {progress.totalTests}
({Math.round((progress.completedTests / progress.totalTests) * 100)}%)
</span>
</div>
</div>
)
}