/* ========================================================================== 阅间 · 数据层 -------------------------------------------------------------------------- 把后端 /api/reading/* 接口封装成前端可直接使用的数据结构。 设计稿(Design Demo)原本使用 demo-data.js 里的静态示例数据, 这里替换为真实接口调用,字段名与设计稿保持一致。 ========================================================================== */ (function (global) { 'use strict'; const BASE = '/api/reading'; async function request(path, params, method = 'GET', body) { const url = new URL(BASE + path, global.location.origin); Object.entries(params || {}).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') url.searchParams.set(key, value); }); const response = await fetch(url.toString(), { method, headers: { Accept: 'application/json', ...(body ? { 'Content-Type': 'application/json' } : {}) }, ...(body ? { body: JSON.stringify(body) } : {}), }); let payload = null; try { payload = await response.json(); } catch (error) { payload = null; } if (!response.ok) { const message = (payload && (payload.error || payload.detail)) || `请求失败(${response.status})`; const err = new Error(message); err.status = response.status; err.payload = payload; throw err; } return payload; } /* ------------------------------ 工具函数 ------------------------------ */ function durationText(seconds) { const total = Math.max(0, Math.round(Number(seconds) || 0)); const hours = Math.floor(total / 3600); const minutes = Math.round((total % 3600) / 60); return hours ? `${hours}小时${minutes}分钟` : `${minutes}分钟`; } function percentText(value) { const num = Number(value) || 0; return `${num}%`; } /* ------------------------------ 接口封装 ------------------------------ */ const WeApi = { durationText, percentText, status() { return request('/status'); }, goals() { return request('/goals'); }, saveGoal(goal, editing = false) { return request(editing ? `/goals/${encodeURIComponent(goal.id)}` : '/goals', null, editing ? 'PUT' : 'POST', goal); }, deleteGoal(id) { return request(`/goals/${encodeURIComponent(id)}`, null, 'DELETE'); }, importGoals(goals) { return request('/goals/import', null, 'POST', { goals }); }, /** * 书架数据 * @param {string} tab all|reading|done|wish * @param {{keyword?:string, limit?:number, offset?:number, progress?:boolean}} options */ shelf(tab, options) { const opts = options || {}; return request('/shelf', { tab: tab || 'all', keyword: opts.keyword || '', limit: opts.limit, offset: opts.offset, progress: opts.progress ? 1 : 0, }); }, dashboard(period) { return request('/dashboard', { period: period || 'week' }); }, notebooks() { return request('/notebooks'); }, /** bookId 传 'all' 时聚合笔记最多的前 limit 本 */ notes(bookId, limit) { return request('/notes', { book_id: bookId, limit: limit }); }, book(bookId) { return request('/book/' + encodeURIComponent(bookId)); }, reviews(bookId, type, count) { return request(`/book/${encodeURIComponent(bookId)}/reviews`, { type: type || 0, count: count || 10 }); }, highlights(bookId, chapterUid) { return request(`/book/${encodeURIComponent(bookId)}/highlights`, { chapter_uid: chapterUid || 0 }); }, underlineThoughts(bookId, chapterUid, range) { return request(`/book/${encodeURIComponent(bookId)}/underline-thoughts`, { chapter_uid: chapterUid, range: range, }); }, thought(reviewId) { return request('/thought/' + encodeURIComponent(reviewId)); }, archives() { return request('/archives'); }, recommend(maxIdx, refresh = false) { return request('/recommend', { maxIdx: maxIdx || 0, refresh: refresh ? 1 : 0 }); }, search(keyword, scope) { return request('/search', { keyword: keyword, scope: scope || 'books' }); }, }; global.WeApi = WeApi; })(window);