166 lines
5.4 KiB
TypeScript
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>
|
|
)
|
|
} |