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"}
--- /dev/null
+/* 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;
+ }
+}
--- /dev/null
+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 (
+ <div className="cal-month" key={monthStr}>
+ <div className="cal-month-title">
+ {y}年 {m}月 · <span className="cal-month-count">(学习了{studiedCount}个group)</span>
+ </div>
+ <div className="cal-grid">
+ {WEEKDAYS.map(w => <div className="cal-weekday" key={w}>{w}</div>)}
+ {cells.map((d, i) => {
+ if (d === null) return <div className="cal-day empty" key={i} />;
+ 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 (
+ <div className={cls.join(' ')} key={i}>
+ <div className="cal-day-num">{d}</div>
+ {acts && acts.map((a, j) => (
+ <div className="cal-act" key={j} style={{ color: OP_COLOR[a.op_type] || '#475569' }}>
+ {OP_LABEL[a.op_type] || a.op_type} G{a.groups.join(',')}
+ </div>
+ ))}
+ </div>
+ );
+ })}
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <div className="cal-page">
+ <div className="cal-container">
+ <h1 className="cal-title">📅 学习日历</h1>
+ {renderMonths.map(renderMonth)}
+ </div>
+ <button className="back-home" onClick={() => navigate('/')}>返回首页</button>
+ </div>
+ );
+}
+
+export default StudyCalendar;