From 2a625ea2737f0f0c4af6dd469b02c5c321049a48 Mon Sep 17 00:00:00 2001 From: lexicon Date: Thu, 27 Aug 2026 14:12:46 +0800 Subject: [PATCH] =?utf8?q?split:=20=E6=8B=86=E5=88=86=E7=BB=84=20new=5Fgro?= =?utf8?q?up=5Fid=20=E5=8A=9F=E8=83=BD(=E5=90=8E=E7=AB=AF=E5=8D=95?= =?utf8?q?=E4=BD=8D=E6=94=B9=E4=B8=BA=20new=5Fgroup=5Fid=20+=20=E7=BB=A7?= =?utf8?q?=E6=89=BF=E5=B1=95=E5=BC=80;=20=E5=89=8D=E7=AB=AF=E6=8C=89=20new?= =?utf8?q?=5Fgroup=5Fid=20=E5=B1=95=E7=A4=BA/=E5=AD=A6=E4=B9=A0/=E6=97=A5?= =?utf8?q?=E5=8E=86=E5=8F=8C=E8=AE=A1=E6=95=B0;=20=E7=8B=AC=E7=AB=8B=20DB?= =?utf8?q?=20=E5=90=8D=E4=B8=8E=20/split=20=E9=83=A8=E7=BD=B2=E9=85=8D?= =?utf8?q?=E7=BD=AE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- backend/db/bcd.db | Bin 9474048 -> 9519104 bytes backend/main.py | 236 ++++++++++++++++------- frontend/package.json | 1 + frontend/public/sw.js | 21 +- frontend/src/App.js | 2 +- frontend/src/api.js | 5 +- frontend/src/components/Home.css | 6 + frontend/src/components/Home.js | 16 +- frontend/src/components/StudyCalendar.js | 26 ++- frontend/src/components/StudyMode.js | 2 +- frontend/src/components/TestMode.js | 2 +- frontend/src/db.js | 54 ++++-- frontend/src/index.js | 4 +- frontend/src/sync.js | 37 ++-- 14 files changed, 281 insertions(+), 131 deletions(-) diff --git a/backend/db/bcd.db b/backend/db/bcd.db index 98963a14009a54751bc95db574fd2a0ce6f18eb8..2d262433b68a43bce2ed0e3cf958ffea4eafacb6 100644 GIT binary patch delta 62162 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 delta 52643 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 diff --git a/backend/main.py b/backend/main.py index fd91b1f..b166364 100644 --- a/backend/main.py +++ b/backend/main.py @@ -107,22 +107,25 @@ class WordData(BaseModel): example_sentence: Optional[str] = None picture_id: Optional[int] = None groupid: Optional[int] = None + new_group_id: Optional[str] = None class Config: from_attributes = True class ProgressUpdate(BaseModel): - group_id: Union[str, int] + group_id: Optional[Union[str, int]] = None + new_group_id: Optional[str] = None op_type: str duration_sec: int = 0 @field_validator('group_id', mode='before') @classmethod def coerce_group_id(cls, v): - return str(v) + return str(v) if v is not None else None class SyncEvent(BaseModel): - group_id: Union[str, int] + group_id: Optional[Union[str, int]] = None + new_group_id: Optional[str] = None op_type: str completed_at: str duration_sec: int = 0 @@ -130,7 +133,7 @@ class SyncEvent(BaseModel): @field_validator('group_id', mode='before') @classmethod def coerce_group_id(cls, v): - return str(v) + return str(v) if v is not None else None class LoginRequest(BaseModel): @@ -182,6 +185,66 @@ def get_progress_from_db(): print(f"Database error in get_progress_from_db: {e}") return progress +def _split_parent_subunits(conn): + # 返回 {groupid: [new_group_id,...]} 仅含"被拆分的父组"(同一 groupid 对应 >1 个 new_group_id) + cur = conn.cursor() + cur.execute("SELECT groupid, new_group_id FROM words WHERE new_group_id IS NOT NULL") + by_parent = {} + for gid, ng in cur.fetchall(): + by_parent.setdefault(str(gid), []).append(ng) + return {g: subs for g, subs in by_parent.items() if len(set(subs)) > 1} + + +def get_effective_progress(): + # split 视图的"有效完成进度": + # 键 = new_group_id(拆分组); 值 = {op_type: (date, duration_sec)} + # 含"父组完成 → 各子单元继承"展开: 若某 groupid 被拆分, 且其本身已有某 op 完成记录, + # 则每个子单元 new_group_id 也视为完成(用于早期已学完的拆分组 G25/G46/G47 等)。 + if not os.path.exists(DATABASE_PATH): + return {} + conn = sqlite3.connect(DATABASE_PATH) + conn.row_factory = sqlite3.Row + cur = conn.cursor() + # 真实记录: 按 new_group_id 聚合 + cur.execute(""" + SELECT new_group_id, op_type, MAX(COALESCE(log_date, created_at)), MAX(duration_sec) + FROM study_logs WHERE status='completed' AND new_group_id IS NOT NULL + GROUP BY new_group_id, op_type + """) + eff = {} + for ng, op, date, dur in cur.fetchall(): + eff.setdefault(ng, {})[op] = (date, dur or 0) + # 父组完成情况: 按 group_id 聚合 + cur.execute(""" + SELECT group_id, op_type, MAX(COALESCE(log_date, created_at)) + FROM study_logs WHERE status='completed' GROUP BY group_id, op_type + """) + parent = {} + for gid, op, date in cur.fetchall(): + parent.setdefault(str(gid), {})[op] = date + subs = _split_parent_subunits(conn) + for gid, sub_list in subs.items(): + ops = parent.get(gid, {}) + if not ops: + continue + for ng in sub_list: + if ng not in eff: + eff[ng] = {} + for op, date in ops.items(): + if op not in eff[ng]: + eff[ng][op] = (date, 0) + conn.close() + return eff + + +def _ng_to_parent(ng): + # '69.1' -> 69 ; '55' -> 55 + try: + return int(str(ng).split('.')[0]) + except Exception: + return None + + # --- 路由实现 --- @app.get("/api/home", response_model=HomeData) @@ -200,41 +263,44 @@ async def get_home_data(): @app.get("/api/home/process", response_model=List[ProgressData]) async def get_home_process(): + # split 视图: 以 new_group_id(拆分组) 为行单位, 含父组→子单元继承展开 try: conn = sqlite3.connect(DATABASE_PATH) + conn.row_factory = sqlite3.Row cursor = conn.cursor() - cursor.execute("SELECT DISTINCT groupid FROM words ORDER BY CAST(groupid AS INTEGER) ASC") - all_groups = [str(row[0]) for row in cursor.fetchall()] + cursor.execute("SELECT DISTINCT new_group_id FROM words WHERE new_group_id IS NOT NULL ORDER BY CAST(new_group_id AS REAL) ASC") + all_ng = [r[0] for r in cursor.fetchall()] conn.close() - log_progress = get_progress_from_db() + eff = get_effective_progress() results = [] first_unstudied = None - for g in all_groups: - if g not in log_progress or not log_progress[g]['study']: - first_unstudied = g + for ng in all_ng: + if not eff.get(ng, {}).get('study'): + first_unstudied = ng break - for g_name in all_groups: - p = log_progress.get(g_name, {'study': None, 'test': None, 'review1': None, 'review2': None, 'review3': None}) - row = {"group": f"Group {g_name}"} - if p['study']: - row['study'] = f"✔️ {p['study'][:10]}" - elif g_name == first_unstudied: + for ng in all_ng: + p = eff.get(ng, {}) # 值为 (date, duration) 元组 + d = lambda v: (v[0][:10] if v and isinstance(v, (tuple, list)) and v[0] else '') + row = {"group": f"Group {ng}"} + if p.get('study'): + row['study'] = f"✔️ {d(p['study'])}" + elif ng == first_unstudied: row['study'] = "✅请开始学习" else: row['study'] = "" - if p['test']: - row['test'] = f"✔️ {p['test'][:10]}" - elif p['study']: + if p.get('test'): + row['test'] = f"✔️ {d(p['test'])}" + elif p.get('study'): row['test'] = "✅请开始测试" else: row['test'] = "" - rk, pk = ['review1', 'review2', 'review3'], ['test', 'review1', 'review2'] - for i, key in enumerate(rk): - prev_key = pk[i] - if p[key]: - row[key] = f"✔️ {p[key][:10]}" - elif p[prev_key]: - hours = calculate_hours_since(p[prev_key]) + prev_map = {'review1': 'test', 'review2': 'review1', 'review3': 'review2'} + for key in ['review1', 'review2', 'review3']: + prev = prev_map[key] + if p.get(key): + row[key] = f"✔️ {d(p[key])}" + elif p.get(prev): + hours = calculate_hours_since(d(p[prev])) row[key] = "⚠️请尽快复习" if hours and hours > 4 else "✅请开始复习" else: row[key] = "" @@ -261,9 +327,10 @@ async def get_all_words(): affix, example_sentence, picture_id, - groupid + groupid, + new_group_id FROM words - ORDER BY CAST(groupid AS INTEGER) ASC, ID ASC + ORDER BY CAST(new_group_id AS REAL) ASC, ID ASC """) rows = cursor.fetchall() conn.close() @@ -273,11 +340,11 @@ async def get_all_words(): @app.get("/api/words/{group_id}", response_model=List[WordData]) async def get_words_by_group(group_id: str): + # split 视图: group_id 参数实际传 new_group_id(如 '69.1'), 按 new_group_id 匹配 try: conn = sqlite3.connect(DATABASE_PATH) conn.row_factory = sqlite3.Row cursor = conn.cursor() - # 核心修复:将数据库的 ID 显式重命名为 id 以匹配 Pydantic 模型 cursor.execute(""" SELECT ID as id, @@ -288,9 +355,10 @@ async def get_words_by_group(group_id: str): affix, example_sentence, picture_id, - groupid + groupid, + new_group_id FROM words - WHERE CAST(groupid AS INTEGER) = ? + WHERE new_group_id = ? ORDER BY ID ASC """, (group_id,)) rows = cursor.fetchall() @@ -432,17 +500,23 @@ async def update_progress(data: ProgressUpdate): """) now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") today_prefix = now[:10] - # 去重: 当天已有同 (group_id, op_type) 的完成记录则跳过,防止前端重复提交产生脏数据 + # split: 以 new_group_id 为单位; 未传则回退到 group_id + ng = data.new_group_id or (str(data.group_id) if data.group_id is not None else None) + if not ng: + conn.close() + raise HTTPException(status_code=400, detail="group_id or new_group_id required") + gid = _ng_to_parent(ng) + # 去重: 当天已有同 (new_group_id, op_type) 的完成记录则跳过,防止前端重复提交产生脏数据 cursor.execute( - "SELECT id FROM study_logs WHERE group_id=? AND op_type=? AND status='completed' AND COALESCE(log_date, substr(created_at,1,10))=?", - (data.group_id, data.op_type, today_prefix) + "SELECT id FROM study_logs WHERE new_group_id=? AND op_type=? AND status='completed' AND COALESCE(log_date, substr(created_at,1,10))=?", + (ng, data.op_type, today_prefix) ) if cursor.fetchone(): conn.close() return {"status": "skipped", "date": now, "reason": "duplicate same-day record"} cursor.execute( - "INSERT INTO study_logs (group_id, op_type, status, created_at, log_date, duration_sec) VALUES (?, ?, ?, ?, ?, ?)", - (data.group_id, data.op_type, 'completed', now, today_prefix, data.duration_sec or 0) + "INSERT INTO study_logs (group_id, new_group_id, op_type, status, created_at, log_date, duration_sec) VALUES (?, ?, ?, ?, ?, ?, ?)", + (str(gid) if gid is not None else None, ng, data.op_type, 'completed', now, today_prefix, data.duration_sec or 0) ) conn.commit() conn.close() @@ -452,34 +526,53 @@ async def update_progress(data: ProgressUpdate): @app.get("/api/calendar") async def get_calendar(): - # 按日期聚合 study_logs,供学习日历使用;同时计算每日"实际学习时长"(分钟) - # 实际学习时长 = 当天各 (group,op) 完成时长(duration_sec)之和, 不含间隔, 即把各次学习时段相加 + # split 视图: 按 new_group_id 聚合并展开"父组完成→子单元继承"。 + # 每个 activity 同时返回 groups(new_group_id) 与 orig_groups(原 group_id), + # 供前端月度"X 个组, 包含 Y 个拆分组"双计数。 try: conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() cursor.execute(""" - SELECT COALESCE(log_date, substr(created_at,1,10)) as day, op_type, group_id + SELECT COALESCE(log_date, substr(created_at,1,10)) as day, op_type, new_group_id, group_id FROM study_logs WHERE status='completed' - ORDER BY day, op_type, CAST(group_id AS INTEGER) + ORDER BY day, op_type """) + base = cursor.fetchall() + subs = _split_parent_subunits(conn) + conn.close() + # 展开: 父组(被拆分)的每条完成记录, 复制给其每个子单元 new_group_id + expanded = [] # (day, op, ng, gid) + for day, op_type, ng, gid in base: + ng = ng if ng else str(gid) + expanded.append((day, op_type, ng, str(gid))) + if str(gid) in subs: + for sng in subs[str(gid)]: + if sng != ng: + expanded.append((day, op_type, sng, str(gid))) day_map = {} - for day, op_type, group_id in cursor.fetchall(): - day_map.setdefault(day, {}).setdefault(op_type, []).append(str(group_id)) - # 每日实际学习时长(秒) = 各 (group,op) 完成时长之和 - cursor.execute(""" + for day, op_type, ng, gid in expanded: + d = day_map.setdefault(day, {}) + a = d.setdefault(op_type, {'groups': [], 'orig': set()}) + if ng not in a['groups']: + a['groups'].append(ng) + if gid not in a['orig']: + a['orig'].add(gid) + # 每日实际学习时长(秒) + conn2 = sqlite3.connect(DATABASE_PATH) + cur2 = conn2.cursor() + cur2.execute(""" SELECT COALESCE(log_date, substr(created_at,1,10)) as day, COALESCE(SUM(duration_sec), 0) as total_sec - FROM study_logs - WHERE status='completed' - GROUP BY day + FROM study_logs WHERE status='completed' GROUP BY day """) - day_total = {day: total for day, total in cursor.fetchall()} - conn.close() + day_total = {day: total for day, total in cur2.fetchall()} + conn2.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] + activities = [{"op_type": op, "groups": acts[op]['groups'], "orig_groups": sorted(acts[op]['orig'], key=lambda x: int(x) if x.isdigit() else x)} + for op in op_order if op in acts] item = {"date": day, "activities": activities} total = day_total.get(day, 0) if total and total > 0: @@ -493,9 +586,7 @@ ALLOWED_OP_TYPES = {'study', 'test', 'review1', 'review2', 'review3'} @app.post("/api/sync/push") async def sync_push(data: SyncPushRequest): - # 布尔并集: 每个 (group_id, op_type) 格子,任一方完成即完成。 - # 服务器已有该 (group, op) 的 completed 记录则跳过;否则补写一条。 - # 不删除、不比较时间戳。依赖 UNIQUE(group_id, op_type, log_date) 约束兜底并发。 + # split: 以 new_group_id 为单元做布尔并集; 未传 new_group_id 时回退 group_id。 try: conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() @@ -505,9 +596,14 @@ async def sync_push(data: SyncPushRequest): if op not in ALLOWED_OP_TYPES: bad += 1 continue + ng = ev.new_group_id or (str(ev.group_id) if ev.group_id is not None else None) + if not ng: + bad += 1 + continue + gid = _ng_to_parent(ng) cursor.execute( - "SELECT 1 FROM study_logs WHERE group_id=? AND op_type=? AND status='completed' LIMIT 1", - (ev.group_id, op)) + "SELECT 1 FROM study_logs WHERE new_group_id=? AND op_type=? AND status='completed' LIMIT 1", + (ng, op)) if cursor.fetchone(): skipped += 1 continue @@ -515,9 +611,9 @@ async def sync_push(data: SyncPushRequest): log_date = completed_at[:10] duration_sec = ev.duration_sec or 0 cursor.execute( - "INSERT OR IGNORE INTO study_logs (group_id, op_type, status, created_at, log_date, duration_sec) " - "VALUES (?, ?, 'completed', ?, ?, ?)", - (ev.group_id, op, completed_at, log_date, duration_sec)) + "INSERT OR IGNORE INTO study_logs (group_id, new_group_id, op_type, status, created_at, log_date, duration_sec) " + "VALUES (?, ?, ?, 'completed', ?, ?, ?)", + (str(gid) if gid is not None else None, ng, op, completed_at, log_date, duration_sec)) updated += 1 conn.commit() conn.close() @@ -528,18 +624,20 @@ async def sync_push(data: SyncPushRequest): @app.get("/api/sync/pull") async def sync_pull(): - # 离线版点击"同步"时,拉取服务器端每个 (group, op) 的"最新"完成时间,供本地比对(LWW)。 + # split: 以 new_group_id 为单元拉取有效进度(含父组→子单元继承展开) try: - conn = sqlite3.connect(DATABASE_PATH) - cursor = conn.cursor() - cursor.execute( - "SELECT group_id, op_type, MAX(COALESCE(log_date, created_at)), MAX(duration_sec) " - "FROM study_logs WHERE status='completed' GROUP BY group_id, op_type" - ) - rows = cursor.fetchall() - conn.close() - events = [{"group_id": str(r[0]), "op_type": r[1], "completed_at": r[2], "duration_sec": r[3] or 0} - for r in rows if r[2]] + eff = get_effective_progress() + events = [] + for ng, ops in eff.items(): + gid = _ng_to_parent(ng) + for op, (date, dur) in ops.items(): + events.append({ + "new_group_id": ng, + "group_id": str(gid) if gid is not None else None, + "op_type": op, + "completed_at": date, + "duration_sec": dur, + }) return {"status": "success", "events": events} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) diff --git a/frontend/package.json b/frontend/package.json index 5d481bf..3460d80 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,6 +2,7 @@ "name": "words", "version": "0.1.0", "private": true, + "homepage": "/split", "dependencies": { "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", diff --git a/frontend/public/sw.js b/frontend/public/sw.js index 94ba4c5..3a3ab90 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -5,11 +5,14 @@ // - navigation (HTML) → network-first, 失败回退 cached index.html // - 静态资源 (JS/CSS/img/font) → cache-first -const CACHE_VERSION = 'words-pwa-v2'; +// split 分支专用 SW: 作用域 /split, 缓存版本独立于 V1(words-pwa-v*), +// 并处理 /split/api 前缀(否则 split 的 TTS/图片不会被缓存, 离线无声)。 +const CACHE_VERSION = 'words-split-v1'; +const SUB = '/split'; // 本分支部署子路径 self.addEventListener('install', (event) => { event.waitUntil( - caches.open(CACHE_VERSION).then((cache) => cache.addAll(['/index.html', '/manifest.json'])) + caches.open(CACHE_VERSION).then((cache) => cache.addAll([SUB + '/index.html', SUB + '/manifest.json'])) ); self.skipWaiting(); }); @@ -29,22 +32,24 @@ self.addEventListener('fetch', (event) => { const url = new URL(req.url); const path = url.pathname; + // 兼容 /split 子路径部署: 取 SUB 前缀下的逻辑路径 + const sub = path.startsWith(SUB + '/') ? SUB : ''; + const apiPrefix = sub + '/api'; // 图片与 TTS 音频: cache-first (含跨域, 后端已开 CORS) - // 注意 TTS 现用查询参数形式 /api/tts?w=,pathname 为 /api/tts(无尾斜杠),故不加尾斜杠匹配 - if (path.startsWith('/api/picture') || path.startsWith('/api/tts')) { + if (path.startsWith(sub + '/api/picture') || path.startsWith(sub + '/api/tts')) { event.respondWith(cacheFirst(req)); return; } // 其他 API JSON: 不拦截, 直走网络 (IndexedDB 兜底) - if (path.startsWith('/api/')) { + if (path.startsWith(apiPrefix + '/')) { return; } // 页面导航: network-first, 离线回退 index.html if (req.mode === 'navigate') { - event.respondWith(networkFirst(req)); + event.respondWith(networkFirst(req, sub)); return; } @@ -68,7 +73,7 @@ async function cacheFirst(req) { } } -async function networkFirst(req) { +async function networkFirst(req, sub = '') { const cache = await caches.open(CACHE_VERSION); try { const res = await fetch(req); @@ -76,6 +81,6 @@ async function networkFirst(req) { return res; } catch (e) { const cached = await cache.match(req); - return cached || cache.match('/index.html'); + return cached || cache.match(sub + '/index.html'); } } diff --git a/frontend/src/App.js b/frontend/src/App.js index d17f71d..0e87872 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -15,7 +15,7 @@ function App() { return (
{loggedIn ? ( - + } /> diff --git a/frontend/src/api.js b/frontend/src/api.js index 911d1dd..b0dd44f 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -24,7 +24,7 @@ http.interceptors.response.use( (err) => { if (err.response && err.response.status === 401) { clearToken(); - window.location.href = '/'; + window.location.href = '/split'; } return Promise.reject(err); } @@ -46,7 +46,8 @@ export const api = { getHomeProcess: () => http.get('/api/home/process'), getWords: (groupId) => http.get(`/api/words/${groupId}`), getAllWords: () => http.get('/api/words/all'), - updateProgress: (group_id, op_type) => http.post('/api/progress/update', { group_id, op_type }), + // split: 以 new_group_id 为单元上报进度(后端据此推导父组号) + updateProgress: (new_group_id, op_type) => http.post('/api/progress/update', { new_group_id, op_type }), syncPush: (events) => http.post('/api/sync/push', { events }), syncPull: () => http.get('/api/sync/pull'), getCalendar: () => http.get('/api/calendar'), diff --git a/frontend/src/components/Home.css b/frontend/src/components/Home.css index 1a3db68..6beecbd 100644 --- a/frontend/src/components/Home.css +++ b/frontend/src/components/Home.css @@ -206,4 +206,10 @@ } .logout-btn:hover { background: #eef3fb; + } + + /* split 分支: 拆分组(含 . 的子单元, 如 69.1)组名单元格淡紫背景 */ + .group-name-split { + background-color: #f3e8ff; + font-weight: 600; } \ No newline at end of file diff --git a/frontend/src/components/Home.js b/frontend/src/components/Home.js index 01f71f8..3f53998 100644 --- a/frontend/src/components/Home.js +++ b/frontend/src/components/Home.js @@ -16,16 +16,18 @@ function Home() { const isOnline = useOnlineStatus(); // 离线降级: 从 IndexedDB 构造与 /api/home/process 同格式的数据 (emoji 字符串) + // split: 以 new_group_id(拆分组) 为行单位 const buildOfflineData = async () => { const allWords = await getAllWordsFromIDB(); const progress = await getAllProgress(); const completedByGroup = {}; progress.forEach(p => { - const g = String(p.group_id); + const g = String(p.new_group_id); if (!completedByGroup[g]) completedByGroup[g] = {}; completedByGroup[g][p.op_type] = p.completed_at; }); - const allGroups = Array.from(new Set(allWords.map(w => String(w.groupid)))) + const allGroups = Array.from(new Set(allWords.map(w => String(w.new_group_id)))) + .filter(Boolean) .sort((a, b) => Number(a) - Number(b)); let firstUnstudied = null; for (const g of allGroups) { @@ -175,10 +177,13 @@ function Home() { - {filteredData.map((item, index) => ( + {filteredData.map((item, index) => { + const ng = item.group.replace('Group ', ''); + const isSplit = ng.includes('.'); + return ( handleCellClick(item, 'group')} style={{ cursor: 'pointer' }} > @@ -220,7 +225,8 @@ function Home() { {item.review3} - ))} + ); + })}
diff --git a/frontend/src/components/StudyCalendar.js b/frontend/src/components/StudyCalendar.js index dceb13d..2b4c7f7 100644 --- a/frontend/src/components/StudyCalendar.js +++ b/frontend/src/components/StudyCalendar.js @@ -34,15 +34,17 @@ function StudyCalendar() { const day = (p.completed_at || '').slice(0, 10); if (!day) return; if (!tmp[day]) tmp[day] = {}; - if (!tmp[day][p.op_type]) tmp[day][p.op_type] = []; - const g = String(p.group_id); - if (!tmp[day][p.op_type].includes(g)) tmp[day][p.op_type].push(g); + if (!tmp[day][p.op_type]) tmp[day][p.op_type] = { groups: [], orig: new Set() }; + const ng = String(p.new_group_id != null ? p.new_group_id : p.group_id); + const gid = ng.split('.')[0]; + if (!tmp[day][p.op_type].groups.includes(ng)) tmp[day][p.op_type].groups.push(ng); + if (!tmp[day][p.op_type].orig.has(gid)) tmp[day][p.op_type].orig.add(gid); if (!dayDurSec[day]) dayDurSec[day] = 0; dayDurSec[day] += (p.duration_sec || 0); }); const m = {}; Object.keys(tmp).forEach(day => { - const activities = OP_ORDER.filter(op => tmp[day][op]).map(op => ({ op_type: op, groups: tmp[day][op] })); + const activities = OP_ORDER.filter(op => tmp[day][op]).map(op => ({ op_type: op, groups: tmp[day][op].groups, orig_groups: Array.from(tmp[day][op].orig) })); const totalSec = dayDurSec[day] || 0; const durationMin = totalSec > 0 ? Math.round(totalSec / 60) : 0; m[day] = { activities, durationMin }; @@ -66,17 +68,23 @@ function StudyCalendar() { 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(); + // 统计当月学习情况(split): X=不同原 group 数, Y=不同拆分组(new_group_id)数 + // 仅按 op_type='study' 计"完成/学习了几个组" + const origGroups = new Set(); // 原 group_id + const splitGroups = new Set(); // new_group_id Object.keys(dayMap).forEach(dateStr => { if (dateStr.slice(0, 7) === monthStr) { const info = dayMap[dateStr]; if (info && info.activities) info.activities.forEach(a => { - if (a.op_type === 'study') a.groups.forEach(g => studiedGroups.add(g)); + if (a.op_type === 'study') { + (a.orig_groups || []).forEach(g => origGroups.add(g)); + (a.groups || []).forEach(g => splitGroups.add(g)); + } }); } }); - const studiedCount = studiedGroups.size; + const origCount = origGroups.size; + const splitCount = splitGroups.size; const cells = []; for (let i = 0; i < offset; i++) cells.push(null); for (let d = 1; d <= daysInMonth; d++) cells.push(d); @@ -85,7 +93,7 @@ function StudyCalendar() { return (
- {y}年 {m}月 · (学习了{studiedCount}个group) + {y}年 {m}月 · (学习了{origCount}个group,包含{splitCount}个拆分组)
{WEEKDAYS.map(w =>
{w}
)} diff --git a/frontend/src/components/StudyMode.js b/frontend/src/components/StudyMode.js index 52a1de7..3d6397a 100644 --- a/frontend/src/components/StudyMode.js +++ b/frontend/src/components/StudyMode.js @@ -52,7 +52,7 @@ function StudyMode() { useEffect(() => { if (isOnline) return; // 在线时所有组均可学 getLearnableGroups().then(groups => { - if (!groups.includes(Number(groupId))) { + if (!groups.includes(groupId)) { alert('该组不可离线学习(仅未完成组与最后完成组后的5组可离线学习)'); navigate('/'); } diff --git a/frontend/src/components/TestMode.js b/frontend/src/components/TestMode.js index f228eba..33e1769 100644 --- a/frontend/src/components/TestMode.js +++ b/frontend/src/components/TestMode.js @@ -35,7 +35,7 @@ function TestMode() { useEffect(() => { if (isOnline) return; getLearnableGroups().then(groups => { - if (!groups.includes(Number(groupId))) { + if (!groups.includes(groupId)) { alert('该组不可离线学习(仅未完成组与最后完成组后的5组可离线学习)'); navigate('/'); } diff --git a/frontend/src/db.js b/frontend/src/db.js index a3603ec..321f84d 100644 --- a/frontend/src/db.js +++ b/frontend/src/db.js @@ -1,11 +1,16 @@ -// IndexedDB 封装 for PWA 离线学习 +// IndexedDB 封装 for PWA 离线学习 (split 分支) // 三个 store: -// - words: 全量词库, keyPath=id, 索引 groupid (供按组查询) -// - progress: 离线进度, keyPath=`${group_id}:${op_type}`, {key, group_id, op_type, completed_at, dirty} +// - words: 全量词库, keyPath=id, 索引 new_group_id (按拆分组查询) +// - progress: 离线进度, keyPath=`${new_group_id}:${op_type}`, {key, new_group_id, group_id, op_type, completed_at, dirty} // - meta: 元数据, keyPath=key (lastSyncAt, learnableGroups[]) +// split 使用独立 DB 名, 与 V1(words-pwa) 在同一浏览器源下互不干扰。 -const DB_NAME = 'words-pwa'; -const DB_VERSION = 1; +const DB_NAME = 'words-split-pwa'; +const DB_VERSION = 2; + +function _parentOf(ng) { + try { return String(ng).split('.')[0]; } catch (e) { return String(ng); } +} let dbPromise = null; @@ -17,7 +22,13 @@ function openDB() { const db = e.target.result; if (!db.objectStoreNames.contains('words')) { db.createObjectStore('words', { keyPath: 'id' }) - .createIndex('groupid', 'groupid', { unique: false }); + .createIndex('new_group_id', 'new_group_id', { unique: false }); + } else { + // 升级: 旧库可能没有 new_group_id 索引 + const ws = e.target.transaction.objectStore('words'); + if (!ws.indexNames.contains('new_group_id')) { + ws.createIndex('new_group_id', 'new_group_id', { unique: false }); + } } if (!db.objectStoreNames.contains('progress')) { db.createObjectStore('progress', { keyPath: 'key' }); @@ -57,9 +68,9 @@ export async function putWords(wordsArr) { } export async function getWordsByGroup(groupId) { - // groupid 在 IDB 是数字, URL 参数是字符串, 统一转 Number 查索引 + // split: groupId 实为 new_group_id(如 '69.1'), 按 new_group_id 索引查询 return singleReq('words', 'readonly', s => - s.index('groupid').getAll(IDBKeyRange.only(Number(groupId))) + s.index('new_group_id').getAll(IDBKeyRange.only(String(groupId))) ); } @@ -73,20 +84,29 @@ export async function getAllWordsFromIDB() { // ---------- progress store ---------- -export async function getProgress(group_id, op_type) { - return singleReq('progress', 'readonly', s => s.get(`${group_id}:${op_type}`)); +export async function getProgress(new_group_id, op_type) { + return singleReq('progress', 'readonly', s => s.get(`${new_group_id}:${op_type}`)); } export async function getAllProgress() { return singleReq('progress', 'readonly', s => s.getAll()); } -export async function putProgress(group_id, op_type, completed_at, dirty, duration_sec = 0) { - const key = `${group_id}:${op_type}`; +// split: 进度以 new_group_id 为单元; group_id 由 new_group_id 推导(父组号)一并存储 +export async function putProgress(new_group_id, op_type, completed_at, dirty, duration_sec = 0) { + const key = `${new_group_id}:${op_type}`; const db = await openDB(); return new Promise((resolve, reject) => { const t = db.transaction('progress', 'readwrite'); - t.objectStore('progress').put({ key, group_id: String(group_id), op_type, completed_at, dirty, duration_sec: duration_sec || 0 }); + t.objectStore('progress').put({ + key, + new_group_id: String(new_group_id), + group_id: _parentOf(new_group_id), + op_type, + completed_at, + dirty, + duration_sec: duration_sec || 0, + }); t.oncomplete = () => resolve(); t.onerror = () => reject(t.error); }); @@ -97,8 +117,8 @@ export async function getDirtyProgress() { return all.filter(p => p.dirty); } -export async function clearDirty(group_id, op_type) { - const key = `${group_id}:${op_type}`; +export async function clearDirty(new_group_id, op_type) { + const key = `${new_group_id}:${op_type}`; const db = await openDB(); return new Promise((resolve, reject) => { const t = db.transaction('progress', 'readwrite'); @@ -113,8 +133,8 @@ export async function clearDirty(group_id, op_type) { } // 删除某条进度(用于服务端已删除记录回写本地, 实现撤销/未执行) -export async function deleteProgress(group_id, op_type) { - const key = `${group_id}:${op_type}`; +export async function deleteProgress(new_group_id, op_type) { + const key = `${new_group_id}:${op_type}`; return singleReq('progress', 'readwrite', s => s.delete(key)); } diff --git a/frontend/src/index.js b/frontend/src/index.js index 2b8b867..f24def9 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -17,10 +17,10 @@ root.render( // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); -// 注册 Service Worker (PWA 离线支持) +// 注册 Service Worker (PWA 离线支持) — split 用相对路径, 确保在 /split 作用域下注册 if ('serviceWorker' in navigator) { window.addEventListener('load', () => { - navigator.serviceWorker.register('/sw.js').catch(err => console.error('SW registration failed:', err)); + navigator.serviceWorker.register('sw.js').catch(err => console.error('SW registration failed:', err)); // SW 接管页面后, 再触发一次同步+预取, 确保预取请求被 SW 拦截并写入缓存 navigator.serviceWorker.ready.then(() => { if (isLoggedIn()) { diff --git a/frontend/src/sync.js b/frontend/src/sync.js index 99df80e..9adb891 100644 --- a/frontend/src/sync.js +++ b/frontend/src/sync.js @@ -7,7 +7,7 @@ import { api } from './api'; import { getDirtyProgress, clearDirty, getAllProgress, putProgress, - putWords, setMeta, getMeta, setLearnableGroups, getAllWordsFromIDB, getWordsCount, + putWords, setMeta, setLearnableGroups, getAllWordsFromIDB, getWordsCount, } from './db'; const OPS = ['study', 'test', 'review1', 'review2', 'review3']; @@ -41,6 +41,7 @@ export async function syncNow() { setSyncStatus('syncing', { phase: 'upload', dirty: dirty.length }); if (dirty.length > 0) { const events = dirty.map(p => ({ + new_group_id: p.new_group_id, group_id: p.group_id, op_type: p.op_type, completed_at: p.completed_at, @@ -49,8 +50,8 @@ export async function syncNow() { const r = await api.syncPush(events); result.pushed = r.data.updated || events.length; result.pushedEvents = events; - // 仅清"已确认上传"的 dirty (云端最终存在该 group/op 即清) - await Promise.all(dirty.map(p => clearDirty(p.group_id, p.op_type))); + // 仅清"已确认上传"的 dirty (云端最终存在该 new_group_id/op 即清) + await Promise.all(dirty.map(p => clearDirty(p.new_group_id, p.op_type))); } // 阶段2: 拉云端进度, 布尔并集合并 (只补不覆盖, 不动本地已有) @@ -59,11 +60,11 @@ export async function syncNow() { const serverEvents = pullResp.data.events || []; const localAll = await getAllProgress(); for (const ev of serverEvents) { - const key = `${ev.group_id}:${ev.op_type}`; + const key = `${ev.new_group_id}:${ev.op_type}`; const local = localAll.find(p => p.key === key); if (!local) { // 本地缺失: 直接补入(布尔并集) - await putProgress(ev.group_id, ev.op_type, ev.completed_at, false, ev.duration_sec || 0); + await putProgress(ev.new_group_id, ev.op_type, ev.completed_at, false, ev.duration_sec || 0); result.pulled++; result.pulledEvents.push(ev); } else { @@ -72,7 +73,7 @@ export async function syncNow() { const localDate = (local.completed_at || '').slice(0, 10); const serverDate = (ev.completed_at || '').slice(0, 10); if (serverDate > localDate) { - await putProgress(ev.group_id, ev.op_type, ev.completed_at, false, ev.duration_sec || 0); + await putProgress(ev.new_group_id, ev.op_type, ev.completed_at, false, ev.duration_sec || 0); result.pulled++; result.pulledEvents.push(ev); } @@ -119,18 +120,20 @@ export async function triggerAutoSync() { } } -// 重算可学习范围: 未完全完成的组 + 最后完成组之后的 5 组 +// 重算可学习范围(split: 以 new_group_id 为单元): 未完全完成的拆分组 + 最后完成组之后的 5 组 export async function recomputeLearnableGroups() { const allWords = await getAllWordsFromIDB(); if (!allWords || allWords.length === 0) { await setLearnableGroups([]); return; } - const allGroups = Array.from(new Set(allWords.map(w => Number(w.groupid)))).sort((a, b) => a - b); + const allGroups = Array.from(new Set(allWords.map(w => String(w.new_group_id)))) + .filter(Boolean) + .sort((a, b) => Number(a) - Number(b)); const progress = await getAllProgress(); const completedByGroup = {}; progress.forEach(p => { - const g = Number(p.group_id); + const g = String(p.new_group_id); if (!completedByGroup[g]) completedByGroup[g] = new Set(); completedByGroup[g].add(p.op_type); }); @@ -142,19 +145,19 @@ export async function recomputeLearnableGroups() { let maxComplete = -1; allGroups.forEach(g => { if (isComplete(g)) { - if (g > maxComplete) maxComplete = g; + if (Number(g) > maxComplete) maxComplete = Number(g); } else { learnable.add(g); } }); // 最后完成组后的 5 组 if (maxComplete >= 0) { - const idx = allGroups.indexOf(maxComplete); + const idx = allGroups.indexOf(String(maxComplete)); for (let i = 1; i <= 5; i++) { if (idx + i < allGroups.length) learnable.add(allGroups[idx + i]); } } - await setLearnableGroups(Array.from(learnable).sort((a, b) => a - b)); + await setLearnableGroups(Array.from(learnable).sort((a, b) => Number(a) - Number(b))); } // ---------- 预取离线资源(思维导图 + 语音)---------- @@ -171,10 +174,12 @@ function prefetchUrl(url) { } function computePrefetchGroups(allWords, progress) { - const allGroups = Array.from(new Set(allWords.map(w => Number(w.groupid)))).sort((a, b) => a - b); + const allGroups = Array.from(new Set(allWords.map(w => String(w.new_group_id)))) + .filter(Boolean) + .sort((a, b) => Number(a) - Number(b)); const opsByGroup = {}; progress.forEach(p => { - const g = Number(p.group_id); + const g = String(p.new_group_id); if (!opsByGroup[g]) opsByGroup[g] = new Set(); opsByGroup[g].add(p.op_type); }); @@ -182,7 +187,7 @@ function computePrefetchGroups(allWords, progress) { const incomplete = allGroups.filter(g => opsByGroup[g] && !isComplete(g)); // 已开始但未完成 const notStarted = allGroups.filter(g => !opsByGroup[g]); // 完全无记录 const firstNotStarted = notStarted.slice(0, PREFETCH_NOT_STARTED_AHEAD); - return Array.from(new Set([...incomplete, ...firstNotStarted])).sort((a, b) => a - b); + return Array.from(new Set([...incomplete, ...firstNotStarted])).sort((a, b) => Number(a) - Number(b)); } export async function prefetchLearnableAssets() { @@ -196,7 +201,7 @@ export async function prefetchLearnableAssets() { const byGroup = {}; allWords.forEach(w => { - const g = Number(w.groupid); + const g = String(w.new_group_id); if (!byGroup[g]) byGroup[g] = []; byGroup[g].push(w); }); -- 2.43.0