Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect, useRef } from 'react'; | |
| function Records() { | |
| const [filter, setFilter] = useState('all'); | |
| const [sessions, setSessions] = useState([]); | |
| const [loading, setLoading] = useState(false); | |
| const chartRef = useRef(null); | |
| // 格式化时间 | |
| const formatDuration = (seconds) => { | |
| const mins = Math.floor(seconds / 60); | |
| const secs = seconds % 60; | |
| return `${mins}m ${secs}s`; | |
| }; | |
| // 格式化日期 | |
| const formatDate = (dateString) => { | |
| const date = new Date(dateString); | |
| return date.toLocaleDateString('en-US', { | |
| month: 'short', | |
| day: 'numeric', | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }); | |
| }; | |
| // 加载会话数据 | |
| const loadSessions = async (filterType) => { | |
| setLoading(true); | |
| try { | |
| const response = await fetch(`/api/sessions?filter=${filterType}&limit=50`); | |
| const data = await response.json(); | |
| setSessions(data); | |
| drawChart(data); | |
| } catch (error) { | |
| console.error('Failed to load sessions:', error); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| // 绘制图表 | |
| const drawChart = (data) => { | |
| const canvas = chartRef.current; | |
| if (!canvas) return; | |
| const ctx = canvas.getContext('2d'); | |
| const width = canvas.width = canvas.offsetWidth; | |
| const height = canvas.height = 300; | |
| // 清空画布 | |
| ctx.clearRect(0, 0, width, height); | |
| if (data.length === 0) { | |
| ctx.fillStyle = '#999'; | |
| ctx.font = '16px Nunito'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('No data available', width / 2, height / 2); | |
| return; | |
| } | |
| // 准备数据 (最多显示最近20个会话) | |
| const displayData = data.slice(0, 20).reverse(); | |
| const padding = 50; | |
| const chartWidth = width - padding * 2; | |
| const chartHeight = height - padding * 2; | |
| const barWidth = chartWidth / displayData.length; | |
| // 找到最大值用于缩放 | |
| const maxScore = 1.0; | |
| // 绘制坐标轴 | |
| ctx.strokeStyle = '#E0E0E0'; | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); | |
| ctx.moveTo(padding, padding); | |
| ctx.lineTo(padding, height - padding); | |
| ctx.lineTo(width - padding, height - padding); | |
| ctx.stroke(); | |
| // 绘制Y轴刻度 | |
| ctx.fillStyle = '#666'; | |
| ctx.font = '12px Nunito'; | |
| ctx.textAlign = 'right'; | |
| for (let i = 0; i <= 4; i++) { | |
| const y = height - padding - (chartHeight * i / 4); | |
| const value = (maxScore * i / 4 * 100).toFixed(0); | |
| ctx.fillText(value + '%', padding - 10, y + 4); | |
| // 绘制网格线 | |
| ctx.strokeStyle = '#F0F0F0'; | |
| ctx.lineWidth = 1; | |
| ctx.beginPath(); | |
| ctx.moveTo(padding, y); | |
| ctx.lineTo(width - padding, y); | |
| ctx.stroke(); | |
| } | |
| // 绘制柱状图 | |
| displayData.forEach((session, index) => { | |
| const barHeight = (session.focus_score / maxScore) * chartHeight; | |
| const x = padding + index * barWidth + barWidth * 0.1; | |
| const y = height - padding - barHeight; | |
| const barActualWidth = barWidth * 0.8; | |
| // 根据分数设置颜色 - 使用蓝色主题 | |
| const score = session.focus_score; | |
| let color; | |
| if (score >= 0.8) color = '#4A90E2'; | |
| else if (score >= 0.6) color = '#5DADE2'; | |
| else if (score >= 0.4) color = '#85C1E9'; | |
| else color = '#AED6F1'; | |
| ctx.fillStyle = color; | |
| ctx.fillRect(x, y, barActualWidth, barHeight); | |
| // 绘制边框 | |
| ctx.strokeStyle = color; | |
| ctx.lineWidth = 1; | |
| ctx.strokeRect(x, y, barActualWidth, barHeight); | |
| }); | |
| // 绘制图例 | |
| ctx.textAlign = 'left'; | |
| ctx.font = 'bold 14px Nunito'; | |
| ctx.fillStyle = '#4A90E2'; | |
| ctx.fillText('Focus Score by Session', padding, 30); | |
| }; | |
| // 初始加载 | |
| useEffect(() => { | |
| loadSessions(filter); | |
| }, [filter]); | |
| // 处理筛选按钮点击 | |
| const handleFilterClick = (filterType) => { | |
| setFilter(filterType); | |
| }; | |
| // 查看详情 | |
| const handleViewDetails = (sessionId) => { | |
| // 这里可以实现查看详情的功能,比如弹窗显示该会话的详细信息 | |
| alert(`View details for session ${sessionId}\n(Feature can be extended later)`); | |
| }; | |
| return ( | |
| <main id="page-d" className="page"> | |
| <h1 className="page-title">My Records</h1> | |
| <div className="records-controls" style={{ display: 'flex', justifyContent: 'center', gap: '10px', marginBottom: '30px' }}> | |
| <button | |
| id="filter-today" | |
| onClick={() => handleFilterClick('today')} | |
| style={{ | |
| padding: '10px 30px', | |
| borderRadius: '8px', | |
| border: filter === 'today' ? 'none' : '2px solid #4A90E2', | |
| background: filter === 'today' ? '#4A90E2' : 'transparent', | |
| color: filter === 'today' ? 'white' : '#4A90E2', | |
| fontSize: '14px', | |
| fontWeight: '500', | |
| cursor: 'pointer', | |
| transition: 'all 0.3s' | |
| }} | |
| > | |
| Today | |
| </button> | |
| <button | |
| id="filter-week" | |
| onClick={() => handleFilterClick('week')} | |
| style={{ | |
| padding: '10px 30px', | |
| borderRadius: '8px', | |
| border: filter === 'week' ? 'none' : '2px solid #4A90E2', | |
| background: filter === 'week' ? '#4A90E2' : 'transparent', | |
| color: filter === 'week' ? 'white' : '#4A90E2', | |
| fontSize: '14px', | |
| fontWeight: '500', | |
| cursor: 'pointer', | |
| transition: 'all 0.3s' | |
| }} | |
| > | |
| This Week | |
| </button> | |
| <button | |
| id="filter-month" | |
| onClick={() => handleFilterClick('month')} | |
| style={{ | |
| padding: '10px 30px', | |
| borderRadius: '8px', | |
| border: filter === 'month' ? 'none' : '2px solid #4A90E2', | |
| background: filter === 'month' ? '#4A90E2' : 'transparent', | |
| color: filter === 'month' ? 'white' : '#4A90E2', | |
| fontSize: '14px', | |
| fontWeight: '500', | |
| cursor: 'pointer', | |
| transition: 'all 0.3s' | |
| }} | |
| > | |
| This Month | |
| </button> | |
| <button | |
| id="filter-all" | |
| onClick={() => handleFilterClick('all')} | |
| style={{ | |
| padding: '10px 30px', | |
| borderRadius: '8px', | |
| border: filter === 'all' ? 'none' : '2px solid #4A90E2', | |
| background: filter === 'all' ? '#4A90E2' : 'transparent', | |
| color: filter === 'all' ? 'white' : '#4A90E2', | |
| fontSize: '14px', | |
| fontWeight: '500', | |
| cursor: 'pointer', | |
| transition: 'all 0.3s' | |
| }} | |
| > | |
| All Time | |
| </button> | |
| </div> | |
| <div className="chart-container" style={{ | |
| background: 'white', | |
| padding: '20px', | |
| borderRadius: '10px', | |
| marginBottom: '30px', | |
| boxShadow: '0 2px 8px rgba(0,0,0,0.1)' | |
| }}> | |
| <canvas ref={chartRef} id="focus-chart" style={{ width: '100%', height: '300px' }}></canvas> | |
| </div> | |
| <div className="sessions-list" style={{ | |
| background: 'white', | |
| padding: '20px', | |
| borderRadius: '10px', | |
| boxShadow: '0 2px 8px rgba(0,0,0,0.1)' | |
| }}> | |
| <h2 style={{ color: '#333', marginBottom: '20px', fontSize: '18px', fontWeight: '600' }}>Recent Sessions</h2> | |
| {loading ? ( | |
| <div style={{ textAlign: 'center', padding: '40px', color: '#999' }}> | |
| Loading sessions... | |
| </div> | |
| ) : sessions.length === 0 ? ( | |
| <div style={{ textAlign: 'center', padding: '40px', color: '#999' }}> | |
| No sessions found for this period. | |
| </div> | |
| ) : ( | |
| <table id="sessions-table" style={{ width: '100%', borderCollapse: 'collapse', borderRadius: '10px', overflow: 'hidden' }}> | |
| <thead> | |
| <tr style={{ background: '#4A90E2' }}> | |
| <th style={{ padding: '15px', textAlign: 'left', color: 'white', fontWeight: '600', fontSize: '14px' }}>Date</th> | |
| <th style={{ padding: '15px', textAlign: 'center', color: 'white', fontWeight: '600', fontSize: '14px' }}>Duration</th> | |
| <th style={{ padding: '15px', textAlign: 'center', color: 'white', fontWeight: '600', fontSize: '14px' }}>Focus Score</th> | |
| <th style={{ padding: '15px', textAlign: 'center', color: 'white', fontWeight: '600', fontSize: '14px' }}>Action</th> | |
| </tr> | |
| </thead> | |
| <tbody id="sessions-tbody"> | |
| {sessions.map((session, index) => ( | |
| <tr key={session.id} style={{ | |
| background: index % 2 === 0 ? '#f8f9fa' : 'white', | |
| borderBottom: '1px solid #e9ecef' | |
| }}> | |
| <td style={{ padding: '15px', color: '#333', fontSize: '13px' }}>{formatDate(session.start_time)}</td> | |
| <td style={{ padding: '15px', textAlign: 'center', color: '#333', fontSize: '13px' }}>{formatDuration(session.duration_seconds)}</td> | |
| <td style={{ padding: '15px', textAlign: 'center' }}> | |
| <span | |
| style={{ | |
| color: | |
| session.focus_score >= 0.8 | |
| ? '#28a745' | |
| : session.focus_score >= 0.6 | |
| ? '#ffc107' | |
| : session.focus_score >= 0.4 | |
| ? '#fd7e14' | |
| : '#dc3545', | |
| fontWeight: '600', | |
| fontSize: '13px' | |
| }} | |
| > | |
| {(session.focus_score * 100).toFixed(1)}% | |
| </span> | |
| </td> | |
| <td style={{ padding: '15px', textAlign: 'center' }}> | |
| <button | |
| onClick={() => handleViewDetails(session.id)} | |
| style={{ | |
| padding: '6px 20px', | |
| background: '#4A90E2', | |
| border: 'none', | |
| color: 'white', | |
| borderRadius: '6px', | |
| cursor: 'pointer', | |
| fontSize: '12px', | |
| fontWeight: '500', | |
| transition: 'background 0.3s' | |
| }} | |
| onMouseOver={(e) => e.target.style.background = '#357ABD'} | |
| onMouseOut={(e) => e.target.style.background = '#4A90E2'} | |
| > | |
| View | |
| </button> | |
| </td> | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| )} | |
| </div> | |
| </main> | |
| ); | |
| } | |
| export default Records; | |