feat(web): v2 看板前端接入真实数据接口

前端样式完全沿用 2.0 设计稿,仅替换数据来源。

- templates/index.html 改用设计稿 DOM 结构与类名,引用 styles.css + weapi.js + app.js
- static/css/styles.css 引入 2.0 设计稿样式
- static/js/weapi.js 新增,封装 /api/v2/* 调用为全局 WeApi
- static/js/app.js 重写:保留设计稿全部渲染函数与 CSS 类名,
  数据源由 demo-data.js 静态假数据改为真实接口,
  补充异步加载器、加载/错误状态机与重试按钮
- static/js/demo-data.js 保留为设计稿原始假数据对照
This commit is contained in:
admin committed 2026-09-11 18:21:06 +08:00
1 parent a114f3f407
commit 8af75454a4
5 files changed
+804 -1441

No files matched your search

+630 -1233
View File
File diff suppressed because it is too large. Load diff
+25
View File
@@ -0,0 +1,25 @@
// 所有数据均为设计示例,不来自真实账号。
const books = [{title:'也许你该找个人聊聊',short:'也许你该\n找个人\n聊聊',author:'洛莉·戈特利布',en:'MAYBE YOU SHOULD TALK TO SOMEONE',cover:'c1',progress:68,status:'reading',score:'9.1',desc:'一位心理治疗师的回忆录。跟随几个普通人的故事,理解关系、失去与成长,也重新认识自己。'},{title:'三体',short:'三体',author:'刘慈欣',en:'THE THREE-BODY PROBLEM',cover:'c2',progress:42,status:'reading',score:'9.4',desc:'从一段隐秘的往事出发,走进人类与宇宙文明的宏大故事。科学、想象与命运交织,打开另一种观看世界的尺度。'},{title:'蛤蟆先生去看心理医生',short:'蛤蟆先生\n去看\n心理医生',author:'罗伯特·戴博德',en:'COUNSELLING FOR TOADS',cover:'c3',progress:85,status:'reading',score:'8.8',desc:'借由蛤蟆先生与心理咨询师的对话,探索情绪与行为背后的原因。这是一次关于理解自己、为自己负责的旅程。'},{title:'人生海海',short:'人生\n海海',author:'麦家',en:'LIFE IS A VAST SEA',cover:'c4',progress:26,status:'reading',score:'9.0',desc:'在复杂的时代与人情之中,讲述一个人的秘密与命运。人生如海,有暗流,也有让人继续前行的力量。'},{title:'活着',short:'活着',author:'余华',en:'TO LIVE',cover:'c3',progress:100,status:'done',score:'9.5',desc:'一个人与时代相遇的故事,关于生命的韧性与日常的重量。'},{title:'被讨厌的勇气',short:'被讨厌\n的勇气',author:'岸见一郎 / 古贺史健',en:'THE COURAGE TO BE DISLIKED',cover:'c1',progress:100,status:'done',score:'8.6',desc:'以对话的方式讨论人际关系、自我接纳与选择的自由。'},{title:'悉达多',short:'悉达多',author:'赫尔曼·黑塞',en:'SIDDHARTHA',cover:'c4',progress:0,status:'wish',score:'9.2',desc:'一个寻找自我的人,穿过不同的人生经验,在河流边学会倾听。'},{title:'置身事内',short:'置身\n事内',author:'兰小欢',en:'INSIDE THE SYSTEM',cover:'c2',progress:0,status:'wish',score:'9.3',desc:'从具体的经济生活出发,理解政府与经济发展的关系。'}];
books.forEach((book, index) => { book.id = `b${index}`; });
const booklists = [
{id:'l1',name:'在故事里,读懂人生',source:'我的书单',description:'从别人的故事里,找到自己生活的回声。',ids:['b3','b4','b6']},
{id:'l2',name:'心理学,向内走一程',source:'我的书单',description:'理解情绪与关系,也练习温柔地对待自己。',ids:['b0','b2','b5']},
{id:'l3',name:'重新认识这个世界',source:'公开书单 · 示例',description:'从宇宙到日常,拓宽理解世界的尺度。',ids:['b1','b7','b6']},
{id:'l4',name:'适合慢慢读的夜晚',source:'公开书单 · 示例',description:'把时间留给文学,让心绪落在书页之间。',ids:['b4','b6','b0']}
];
const initialGoals = [
{id:'g1',name:'秋季文学计划',start:'2026-09-01',end:'2026-11-30',ids:['b3','b4','b6','b1'],sources:['在故事里,读懂人生']},
{id:'g2',name:'给内心一点生长的空间',start:'2026-09-01',end:'2026-10-31',ids:['b0','b2','b5'],sources:['心理学,向内走一程']},
{id:'g3',name:'读完的书,成为自己的底气',start:'2026-08-01',end:'2026-08-31',ids:['b4','b5'],sources:[]}
];
const personalNotes = [
{id:'n1',bookId:'b0',type:'thought',text:'把注意力还给生活本身,让平凡的日子有迹可循。',date:'2026-09-11'},
{id:'n2',bookId:'b1',type:'highlight',text:'在辽阔的未知面前,每一个问题都值得被认真对待。',date:'2026-09-10'},
{id:'n3',bookId:'b2',type:'thought',text:'允许情绪出现,也给自己一些理解它的时间。',date:'2026-09-08'},
{id:'n4',bookId:'b3',type:'highlight',text:'人生的答案,或许藏在继续向前的日子里。',date:'2026-09-06'}
];
const publicReviews = [
{id:'r1',name:'山间来信',rating:5,type:'推荐',text:'适合放慢速度去读。最打动我的,是读完之后还会不断回想的那些问题。',date:'2026-09-10'},
{id:'r2',name:'晚风书屋',rating:3,type:'一般',text:'有一些有趣的视角,也有需要耐心消化的地方。每个人会读出不同的感受。',date:'2026-09-09'},
{id:'r3',name:'一页一页',rating:1,type:'不行',text:'这次阅读没有找到共鸣,可能还不是适合我当下阅读的书。',date:'2026-09-08'}
];
+125
View File
@@ -0,0 +1,125 @@
/* ==========================================================================
阅间 · 数据层(v2)
--------------------------------------------------------------------------
把后端 /api/v2/* 接口封装成前端可直接使用的数据结构。
设计稿(Design Demo)原本使用 demo-data.js 里的静态示例数据,
这里替换为真实接口调用,字段名与设计稿保持一致。
========================================================================== */
(function (global) {
'use strict';
const BASE = '/api/v2';
async function request(path, params) {
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(), { headers: { Accept: 'application/json' } });
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');
},
/**
* 书架数据
* @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) {
return request('/recommend', { maxIdx: maxIdx || 0 });
},
search(keyword, scope) {
return request('/search', { keyword: keyword, scope: scope || 'books' });
},
};
global.WeApi = WeApi;
})(window);