@@ -32,6 +32,42 @@ const SEGMENT_GAP = 220;
3232// 节点之间的基础间距
3333const NODE_SPACING = 180 ;
3434
35+ // 智能计算分段,避免最后一段太短
36+ const calculateSegments = ( totalProblems : number ) : number [ ] => {
37+ // 5题及以下:不分段,只有终点宝箱
38+ if ( totalProblems <= 5 ) {
39+ return [ totalProblems ] ;
40+ }
41+
42+ // 6-7题:分成两段(3+3 或 3+4)
43+ if ( totalProblems <= 7 ) {
44+ const firstHalf = Math . floor ( totalProblems / 2 ) ;
45+ return [ firstHalf , totalProblems - firstHalf ] ;
46+ }
47+
48+ // 8题及以上:每5题一段,但确保最后一段至少3题
49+ const segments : number [ ] = [ ] ;
50+ let remaining = totalProblems ;
51+
52+ while ( remaining > 0 ) {
53+ if ( remaining <= 7 ) {
54+ // 剩余7题及以下,平均分成最后两段
55+ if ( remaining <= 5 ) {
56+ segments . push ( remaining ) ;
57+ } else {
58+ const firstHalf = Math . floor ( remaining / 2 ) ;
59+ segments . push ( firstHalf ) ;
60+ segments . push ( remaining - firstHalf ) ;
61+ }
62+ break ;
63+ }
64+ segments . push ( SEGMENT_SIZE ) ;
65+ remaining -= SEGMENT_SIZE ;
66+ }
67+
68+ return segments ;
69+ } ;
70+
3571const DuolingoPath : React . FC < DuolingoPathProps > = ( {
3672 problems,
3773 allProblems,
@@ -84,17 +120,20 @@ const DuolingoPath: React.FC<DuolingoPathProps> = ({
84120 // 计算分段信息和宝箱位置
85121 const segmentInfo = useMemo ( ( ) => {
86122 const totalProblems = problems . length ;
87- const segmentCount = Math . ceil ( totalProblems / SEGMENT_SIZE ) ;
123+ const segmentSizes = calculateSegments ( totalProblems ) ;
124+ const segmentCount = segmentSizes . length ;
88125 const segments : {
89126 startIndex : number ;
90127 endIndex : number ;
91128 completedCount : number ;
92129 isComplete : boolean ;
93130 } [ ] = [ ] ;
94131
132+ let currentIndex = 0 ;
95133 for ( let i = 0 ; i < segmentCount ; i ++ ) {
96- const startIndex = i * SEGMENT_SIZE ;
97- const endIndex = Math . min ( startIndex + SEGMENT_SIZE - 1 , totalProblems - 1 ) ;
134+ const segmentSize = segmentSizes [ i ] ;
135+ const startIndex = currentIndex ;
136+ const endIndex = currentIndex + segmentSize - 1 ;
98137
99138 // 计算该分段的完成数量
100139 let completedCount = 0 ;
@@ -104,33 +143,37 @@ const DuolingoPath: React.FC<DuolingoPathProps> = ({
104143 }
105144 }
106145
107- const segmentSize = endIndex - startIndex + 1 ;
108146 segments . push ( {
109147 startIndex,
110148 endIndex,
111149 completedCount,
112150 isComplete : completedCount === segmentSize
113151 } ) ;
152+
153+ currentIndex = endIndex + 1 ;
114154 }
115155
116- return { segmentCount, segments } ;
156+ return { segmentCount, segments, segmentSizes } ;
117157 } , [ problems , isCompleted ] ) ;
118158
119159 // 基于原始完整题目列表计算宝箱解锁状态(防止筛选后作弊)
120160 const originalSegmentInfo = useMemo ( ( ) => {
121161 const originalProblems = allProblems || problems ;
122162 const totalProblems = originalProblems . length ;
123- const segmentCount = Math . ceil ( totalProblems / SEGMENT_SIZE ) ;
163+ const segmentSizes = calculateSegments ( totalProblems ) ;
164+ const segmentCount = segmentSizes . length ;
124165 const segments : {
125166 startIndex : number ;
126167 endIndex : number ;
127168 completedCount : number ;
128169 isComplete : boolean ;
129170 } [ ] = [ ] ;
130171
172+ let currentIndex = 0 ;
131173 for ( let i = 0 ; i < segmentCount ; i ++ ) {
132- const startIndex = i * SEGMENT_SIZE ;
133- const endIndex = Math . min ( startIndex + SEGMENT_SIZE - 1 , totalProblems - 1 ) ;
174+ const segmentSize = segmentSizes [ i ] ;
175+ const startIndex = currentIndex ;
176+ const endIndex = currentIndex + segmentSize - 1 ;
134177
135178 // 计算该分段的完成数量
136179 let completedCount = 0 ;
@@ -140,16 +183,17 @@ const DuolingoPath: React.FC<DuolingoPathProps> = ({
140183 }
141184 }
142185
143- const segmentSize = endIndex - startIndex + 1 ;
144186 segments . push ( {
145187 startIndex,
146188 endIndex,
147189 completedCount,
148190 isComplete : completedCount === segmentSize
149191 } ) ;
192+
193+ currentIndex = endIndex + 1 ;
150194 }
151195
152- return { segmentCount, segments } ;
196+ return { segmentCount, segments, segmentSizes } ;
153197 } , [ allProblems , problems , isCompleted ] ) ;
154198
155199 // 计算是否所有题目都已完成(基于原始题目列表)
@@ -160,13 +204,25 @@ const DuolingoPath: React.FC<DuolingoPathProps> = ({
160204
161205 // 判断某个索引是否是分段的最后一个节点(不包括整个路径的最后一个)
162206 const isSegmentEnd = useCallback ( ( index : number ) => {
163- return ( index + 1 ) % SEGMENT_SIZE === 0 && index < problems . length - 1 ;
164- } , [ problems . length ] ) ;
207+ // 检查是否是某个分段的最后一个节点
208+ for ( const segment of segmentInfo . segments ) {
209+ if ( index === segment . endIndex && index < problems . length - 1 ) {
210+ return true ;
211+ }
212+ }
213+ return false ;
214+ } , [ segmentInfo . segments , problems . length ] ) ;
165215
166216 // 获取某个索引所在的分段编号
167217 const getSegmentIndex = useCallback ( ( index : number ) => {
168- return Math . floor ( index / SEGMENT_SIZE ) ;
169- } , [ ] ) ;
218+ for ( let i = 0 ; i < segmentInfo . segments . length ; i ++ ) {
219+ const segment = segmentInfo . segments [ i ] ;
220+ if ( index >= segment . startIndex && index <= segment . endIndex ) {
221+ return i ;
222+ }
223+ }
224+ return 0 ;
225+ } , [ segmentInfo . segments ] ) ;
170226
171227 // 简化的蜿蜒路径布局(考虑分段间距)
172228 const getNodePosition = useCallback ( ( index : number ) => {
@@ -184,30 +240,47 @@ const DuolingoPath: React.FC<DuolingoPathProps> = ({
184240
185241 const xPercent = ( xPixel / containerWidth ) * 100 ;
186242
187- // 计算Y位置,考虑分段间距(宝箱节点)
188- const segmentIndex = getSegmentIndex ( index ) ;
243+ // 计算Y位置
244+ // 找出当前节点属于哪个分段,以及之前有多少个完整分段(即多少个宝箱)
245+ let treasureCount = 0 ;
246+ for ( let i = 0 ; i < segmentInfo . segments . length ; i ++ ) {
247+ const seg = segmentInfo . segments [ i ] ;
248+ if ( index > seg . endIndex ) {
249+ // 这个分段已经完全在当前节点之前,且不是最后一个分段,所以有一个宝箱
250+ if ( i < segmentInfo . segments . length - 1 ) {
251+ treasureCount ++ ;
252+ }
253+ }
254+ }
255+
189256 const baseY = index * NODE_SPACING + 100 ;
190- const segmentGapOffset = segmentIndex * SEGMENT_GAP ;
191- const yPosition = baseY + segmentGapOffset ;
257+ const treasureSpaceOffset = treasureCount * SEGMENT_GAP ;
258+ const yPosition = baseY + treasureSpaceOffset ;
192259
193260 return { xPercent, xPixel, yPosition, index } ;
194- } , [ containerWidth , getSegmentIndex ] ) ;
261+ } , [ containerWidth , segmentInfo . segments ] ) ;
195262
196- // 获取宝箱节点位置 - 在分段间隙中居中
263+ // 获取宝箱节点位置 - 在分段末尾节点和下一分段首节点之间的中点
197264 const getTreasurePosition = useCallback ( ( segmentIndex : number ) => {
198- const lastNodeIndex = ( segmentIndex + 1 ) * SEGMENT_SIZE - 1 ;
199- const firstNodeOfNextSegment = ( segmentIndex + 1 ) * SEGMENT_SIZE ;
265+ const segment = segmentInfo . segments [ segmentIndex ] ;
266+ const nextSegment = segmentInfo . segments [ segmentIndex + 1 ] ;
267+
268+ if ( ! segment || ! nextSegment ) {
269+ return { xPercent : 50 , xPixel : containerWidth / 2 , yPosition : 0 } ;
270+ }
200271
201- const lastNodePos = getNodePosition ( Math . min ( lastNodeIndex , problems . length - 1 ) ) ;
202- const nextNodePos = getNodePosition ( Math . min ( firstNodeOfNextSegment , problems . length - 1 ) ) ;
272+ // 计算分段末尾节点的位置(不包含宝箱偏移的原始位置)
273+ const lastNodeBaseY = segment . endIndex * NODE_SPACING + 100 + segmentIndex * SEGMENT_GAP ;
274+ // 计算下一分段首节点的位置
275+ const nextNodeBaseY = nextSegment . startIndex * NODE_SPACING + 100 + ( segmentIndex + 1 ) * SEGMENT_GAP ;
203276
204- // 宝箱放在两个节点的垂直中点
277+ // 宝箱在两者中间
205278 return {
206279 xPercent : 50 ,
207280 xPixel : containerWidth / 2 ,
208- yPosition : ( lastNodePos . yPosition + nextNodePos . yPosition ) / 2
281+ yPosition : ( lastNodeBaseY + nextNodeBaseY ) / 2
209282 } ;
210- } , [ getNodePosition , problems . length , containerWidth ] ) ;
283+ } , [ containerWidth , segmentInfo . segments ] ) ;
211284
212285 // 获取终点宝箱位置
213286 const getEndpointTreasurePosition = useCallback ( ( ) => {
@@ -412,6 +485,12 @@ const DuolingoPath: React.FC<DuolingoPathProps> = ({
412485 const generateTreasureNodes = ( ) => {
413486 const treasures : JSX . Element [ ] = [ ] ;
414487
488+ // 如果是筛选模式(题目数量与原始不同),不显示中间宝箱
489+ const originalProblems = allProblems || problems ;
490+ if ( problems . length !== originalProblems . length ) {
491+ return treasures ;
492+ }
493+
415494 segmentInfo . segments . forEach ( ( segment , segmentIndex ) => {
416495 // 跳过最后一个分段(因为最后有终点标记)
417496 if ( segmentIndex === segmentInfo . segmentCount - 1 ) return ;
@@ -640,8 +719,14 @@ const DuolingoPath: React.FC<DuolingoPathProps> = ({
640719 { generateTreasureNodes ( ) }
641720 </ div >
642721
643- { /* 终点宝箱节点 - 替换原来的静态徽章 */ }
722+ { /* 终点宝箱节点 - 替换原来的静态徽章,筛选模式下隐藏 */ }
644723 { ( ( ) => {
724+ const originalProblems = allProblems || problems ;
725+ // 筛选模式下不显示终点宝箱
726+ if ( problems . length !== originalProblems . length ) {
727+ return null ;
728+ }
729+
645730 const endpointPos = getEndpointTreasurePosition ( ) ;
646731 return (
647732 < div
0 commit comments