From 2975bb44b31c7ed45d4f3ec8304c95c511d59b5d Mon Sep 17 00:00:00 2001 From: words Date: Thu, 30 Jul 2026 11:45:55 +0800 Subject: [PATCH] =?utf8?q?feat:=20=E5=AD=A6=E4=B9=A0=E6=97=A5=E5=8E=86?= =?utf8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit - 后端新增 GET /api/calendar: 按日期聚合 study_logs 返回活动记录 - 前端新建 StudyCalendar 组件: 周一起始日历网格, 当前月置顶, 其余月份倒序 - 有学习记录的日期浅蓝底, 无记录灰底, 今天蓝边框 - 单日全量列出活动: 学/测/R1/R2/R3 + group - 颜色: 学测橙色突出, R1/R2/R3 暗淡蓝绿 - 月份标题显示当月学习 group 数(小字号灰蓝色) - Home.js 加'学习日历'入口按钮, App.js 加 /calendar 路由 --- backend/main.py | 26 ++++ frontend/src/App.js | 2 + frontend/src/components/Home.js | 3 + frontend/src/components/StudyCalendar.css | 146 ++++++++++++++++++++++ frontend/src/components/StudyCalendar.js | 101 +++++++++++++++ 5 files changed, 278 insertions(+) create mode 100644 frontend/src/components/StudyCalendar.css create mode 100644 frontend/src/components/StudyCalendar.js diff --git a/backend/main.py b/backend/main.py index ec6850f..dbf7232 100644 --- a/backend/main.py +++ b/backend/main.py @@ -266,6 +266,32 @@ async def update_progress(data: ProgressUpdate): except Exception as e: raise HTTPException(status_code=500, detail=str(e)) +@app.get("/api/calendar") +async def get_calendar(): + # 按日期聚合 study_logs,供学习日历使用 + try: + conn = sqlite3.connect(DATABASE_PATH) + cursor = conn.cursor() + cursor.execute(""" + SELECT substr(created_at,1,10) as day, op_type, group_id + FROM study_logs + WHERE status='completed' + ORDER BY day, op_type, CAST(group_id AS INTEGER) + """) + day_map = {} + for day, op_type, group_id in cursor.fetchall(): + day_map.setdefault(day, {}).setdefault(op_type, []).append(str(group_id)) + conn.close() + op_order = ['study', 'test', 'review1', 'review2', 'review3'] + result = [] + for day in sorted(day_map.keys()): + acts = day_map[day] + activities = [{"op_type": op, "groups": acts[op]} for op in op_order if op in acts] + result.append({"date": day, "activities": activities}) + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + @app.get("/") async def root(): return {"message": "English Lexicon API is running"} diff --git a/frontend/src/App.js b/frontend/src/App.js index ff960f2..205187a 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -4,6 +4,7 @@ import Home from './components/Home'; import StudyMode from './components/StudyMode'; import TestMode from './components/TestMode'; import GroupView from './components/GroupView'; +import StudyCalendar from './components/StudyCalendar'; function App() { return ( @@ -13,6 +14,7 @@ function App() { } /> } /> } /> + } /> ); diff --git a/frontend/src/components/Home.js b/frontend/src/components/Home.js index 4778bf2..afe151a 100644 --- a/frontend/src/components/Home.js +++ b/frontend/src/components/Home.js @@ -80,6 +80,9 @@ function Home() { +
diff --git a/frontend/src/components/StudyCalendar.css b/frontend/src/components/StudyCalendar.css new file mode 100644 index 0000000..d2ec6b8 --- /dev/null +++ b/frontend/src/components/StudyCalendar.css @@ -0,0 +1,146 @@ +/* StudyCalendar.css - 学习日历 */ + +.cal-page { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + background-color: #f0f7ff; + min-height: 100vh; +} + +.cal-container { + width: 95%; + max-width: 1100px; + display: flex; + flex-direction: column; + gap: 24px; +} + +.cal-title { + text-align: center; + font-size: 22px; + color: #1e40af; + margin: 0; +} + +.cal-month { + background: white; + border-radius: 16px; + box-shadow: 0 8px 24px rgba(17, 24, 39, 0.06); + padding: 20px; +} + +.cal-month-title { + font-size: 18px; + font-weight: 600; + color: #1e40af; + margin-bottom: 12px; + text-align: center; +} + +.cal-month-count { + color: #64748b; + font-size: 14px; + font-weight: 500; +} + +.cal-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 4px; +} + +.cal-weekday { + text-align: center; + font-size: 13px; + font-weight: 600; + color: #1e40af; + background: #eff6ff; + padding: 8px 0; + border-radius: 6px; +} + +.cal-day { + min-height: 110px; + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 6px; + background: #f8fafc; + display: flex; + flex-direction: column; + gap: 3px; + overflow: hidden; +} + +.cal-day.empty { + background: transparent; + border: none; +} + +.cal-day.active { + background: #eff6ff; + border-color: #bfdbfe; +} + +.cal-day.today { + border: 2px solid #1d4ed8; +} + +.cal-day-num { + font-size: 13px; + font-weight: 700; + color: #475569; + text-align: right; +} + +.cal-day.today .cal-day-num { + color: #1d4ed8; +} + +.cal-act { + font-size: 12px; + line-height: 1.3; + font-weight: 500; +} + +.back-home { + margin-top: 20px; + background: none; + border: none; + color: #64748b; + text-decoration: underline; + cursor: pointer; + font-size: 14px; +} + +/* 响应式 */ +@media (max-width: 768px) { + .cal-container { + width: 98%; + } + .cal-day { + min-height: 80px; + padding: 4px; + } + .cal-act { + font-size: 10px; + line-height: 1.2; + } + .cal-day-num { + font-size: 11px; + } + .cal-weekday { + font-size: 11px; + padding: 6px 0; + } + .cal-month-title { + font-size: 16px; + } + .cal-month-count { + font-size: 12px; + } + .cal-title { + font-size: 20px; + } +} diff --git a/frontend/src/components/StudyCalendar.js b/frontend/src/components/StudyCalendar.js new file mode 100644 index 0000000..33940f1 --- /dev/null +++ b/frontend/src/components/StudyCalendar.js @@ -0,0 +1,101 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import axios from 'axios'; +import './StudyCalendar.css'; + +const API_BASE = "http://124.222.157.124:1060"; + +const OP_LABEL = { study: '学', test: '测', review1: 'R1', review2: 'R2', review3: 'R3' }; +const OP_COLOR = { study: '#ea580c', test: '#f97316', review1: '#5b7c99', review2: '#6b8e7a', review3: '#8a9ba8' }; +const WEEKDAYS = ['一', '二', '三', '四', '五', '六', '日']; + +function pad(n) { return String(n).padStart(2, '0'); } + +function StudyCalendar() { + const navigate = useNavigate(); + const [dayMap, setDayMap] = useState({}); + + useEffect(() => { + axios.get(`${API_BASE}/api/calendar`) + .then(res => { + const m = {}; + res.data.forEach(item => { m[item.date] = item.activities; }); + setDayMap(m); + }) + .catch(err => console.error("获取日历失败:", err)); + }, []); + + const today = new Date(); + const todayStr = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`; + const currentMonth = `${today.getFullYear()}-${pad(today.getMonth() + 1)}`; + + // 月份渲染顺序: 当前月 + 有记录月份(去除当前月, 倒序) + const recordMonths = Object.keys(dayMap).map(d => d.slice(0, 7)); + const uniqueRecordMonths = Array.from(new Set(recordMonths)).sort().reverse(); + const renderMonths = [currentMonth, ...uniqueRecordMonths.filter(m => m !== currentMonth)]; + + function renderMonth(monthStr) { + const [yStr, mStr] = monthStr.split('-'); + const y = parseInt(yStr), m = parseInt(mStr); + const firstDay = new Date(y, m - 1, 1).getDay(); // 0=Sun + const offset = (firstDay + 6) % 7; // 周一起始的空格数 + const daysInMonth = new Date(y, m, 0).getDate(); + // 统计当月学习的 group 数(op_type='study' 的不同 group) + const studiedGroups = new Set(); + Object.keys(dayMap).forEach(dateStr => { + if (dateStr.slice(0, 7) === monthStr) { + dayMap[dateStr].forEach(a => { + if (a.op_type === 'study') a.groups.forEach(g => studiedGroups.add(g)); + }); + } + }); + const studiedCount = studiedGroups.size; + const cells = []; + for (let i = 0; i < offset; i++) cells.push(null); + for (let d = 1; d <= daysInMonth; d++) cells.push(d); + while (cells.length % 7 !== 0) cells.push(null); + + return ( +
+
+ {y}年 {m}月 · (学习了{studiedCount}个group) +
+
+ {WEEKDAYS.map(w =>
{w}
)} + {cells.map((d, i) => { + if (d === null) return
; + const dateStr = `${monthStr}-${pad(d)}`; + const acts = dayMap[dateStr]; + const isActive = !!acts; + const isToday = dateStr === todayStr; + const cls = ['cal-day']; + if (isActive) cls.push('active'); + if (isToday) cls.push('today'); + return ( +
+
{d}
+ {acts && acts.map((a, j) => ( +
+ {OP_LABEL[a.op_type] || a.op_type} G{a.groups.join(',')} +
+ ))} +
+ ); + })} +
+
+ ); + } + + return ( +
+
+

📅 学习日历

+ {renderMonths.map(renderMonth)} +
+ +
+ ); +} + +export default StudyCalendar; -- 2.43.0