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
+809 -1446

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
:root{--bg:#f7f8fa;--panel:#fff;--text:#242a34;--muted:#9298a4;--line:#edf0f3;--accent:#356ce5;--soft:#edf3ff;--sidebar:#fff;--shadow:0 8px 32px #24355005;--quote:#f8f6f1;--green:#449682}
[data-theme=dark]{--bg:#14171e;--panel:#1d222c;--text:#e7eaf0;--muted:#8892a4;--line:#2c323f;--accent:#80a5ff;--soft:#26344e;--sidebar:#191d26;--shadow:0 8px 32px #0001;--quote:#292820;--green:#75c6ac;color-scheme:dark}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-family:"PingFang SC","Microsoft YaHei",sans-serif;font-size:14px;transition:background .25s,color .25s}button,input{font:inherit}button{cursor:pointer;color:inherit}button:focus-visible,a:focus-visible,input:focus-visible{outline:3px solid var(--accent);outline-offset:4px}button{border:0;background:none}svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}.sidebar{position:fixed;inset:0 auto 0 0;width:218px;padding:39px 23px 24px;background:var(--sidebar);border-right:1px solid var(--line);display:flex;flex-direction:column;z-index:5}.logo{display:flex;align-items:center;gap:11px;font-size:24px;font-weight:650;letter-spacing:2px}.logo-mark{background:#356ce5;color:white;width:37px;height:37px;display:grid;place-items:center;border-radius:12px}.logo-mark svg{width:25px;height:25px}.logo small{display:block;letter-spacing:2px;font-size:9px;color:var(--muted);margin-top:3px;font-weight:500}.navlabel{font-size:10px;color:var(--muted);letter-spacing:1.7px;margin:53px 14px 15px}.nav{display:flex;flex-direction:column;gap:9px}.nav button{text-align:left;padding:14px 17px;border-radius:9px;display:flex;align-items:center;gap:13px;color:#818997}.nav button.active{color:var(--accent);background:var(--soft);font-weight:600}.nav button:hover{background:var(--soft)}.nav em{margin-left:auto;font-size:10px;font-style:normal;background:var(--line);padding:2px 6px;border-radius:4px}.sidebar-bottom{margin-top:auto}.connection{padding:16px 12px;border:1px solid var(--line);border-radius:10px;font-size:12px;line-height:1.9}.dot{display:inline-block;background:var(--green);width:6px;height:6px;border-radius:50%;margin-right:7px}.connection small{display:block;font-size:10px;color:var(--muted)}.profile{display:flex;align-items:center;gap:10px;margin-top:24px;padding:0 8px}.avatar{width:33px;height:33px;border-radius:50%;display:grid;place-items:center;background:#e4eadf;color:#5e7252;font-family:Georgia;font-size:18px}.profile small{display:block;font-size:10px;color:var(--muted);margin-top:4px}.main{margin-left:218px;padding:0 40px 32px;max-width:1800px}.topbar{height:91px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line)}.breadcrumb{font-size:12px;color:var(--muted)}.breadcrumb span{color:var(--text);margin-left:12px}.top-actions{display:flex;align-items:center;gap:20px}.searchbox{display:flex;align-items:center;gap:10px;color:var(--muted);background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:9px 12px}.searchbox input{background:none;border:0;outline:none;width:180px;color:var(--text);font-size:11px}.key{font-size:10px;border:1px solid var(--line);border-radius:4px;padding:2px 4px}.iconbtn{padding:7px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}.welcome{display:flex;justify-content:space-between;align-items:center;margin:33px 0 27px}.eyebrow{font-size:10px;letter-spacing:2px;color:var(--muted);margin-bottom:10px}h1{font-size:27px;letter-spacing:.5px;margin:0 0 10px;font-weight:600}h1 span{font-family:Georgia;font-size:25px;color:#c3a977}p{margin:0;color:var(--muted);font-size:12px;line-height:1.8}.welcome-right{display:flex;gap:12px;align-items:center}.demo-tag{font-size:10px;padding:6px 9px;background:var(--quote);border:1px solid var(--line);border-radius:5px;color:var(--muted)}.primary{background:#356ce5;color:white;padding:11px 16px;border-radius:7px;font-size:12px}.primary svg{width:15px;height:15px;margin-right:6px}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;margin-bottom:23px}.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.stat{padding:21px 23px}.stat-label{display:flex;justify-content:space-between;align-items:center;color:var(--muted);font-size:12px}.stat-label svg{width:17px;height:17px;color:#9aa8bf}.stat-value{font-family:Georgia,serif;font-size:34px;letter-spacing:-1px;margin:14px 0 12px}.stat-value small{font-family:"PingFang SC",sans-serif;font-size:11px;color:var(--muted);letter-spacing:0;margin-left:5px}.stat-foot{font-size:10px;color:var(--muted)}.stat-foot b{color:var(--green);font-weight:500;margin-right:7px}.columns{display:grid;grid-template-columns:minmax(0,1fr) 296px;gap:23px}.section-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px}.section-head h2{font-size:15px;font-weight:600;margin:0}.section-head h2 small{font-weight:400;color:var(--muted);font-size:10px;margin-left:10px}.text-button{font-size:11px;color:var(--muted);padding:3px}.text-button:hover{color:var(--accent)}.trend{padding:24px 25px;margin-bottom:23px}.segmented{background:var(--bg);padding:3px;display:flex;border-radius:6px}.segmented button{font-size:10px;padding:5px 12px;color:var(--muted);border-radius:4px}.segmented button.active{background:var(--panel);coloLine truncated
/* 阅读目标与内容页:与既有主题变量共用色彩。 */
:root{--muted:#717b8b}[data-theme=dark]{--muted:#a2adbf}h3{font-size:14px;margin:0}button:disabled{opacity:.45;cursor:not-allowed}.demo-banner{display:flex;justify-content:space-between;color:var(--muted);font-size:10px;margin-top:18px}.welcome{margin-top:26px}.page-actions{display:flex;align-items:center;gap:10px}.secondary{padding:10px 15px;border:1px solid var(--line);background:var(--panel);border-radius:7px;font-size:12px}.pill{display:inline-block;padding:5px 8px;font-size:10px;border-radius:5px;background:var(--soft);color:var(--accent);white-space:nowrap}.pill.green{background:#6a9a8219;color:var(--green)}.pill.amber{background:#c3923919;color:#b78a3f}.hint{font-size:11px;color:var(--muted);line-height:1.8}.panel{padding:24px}.panel+.panel{margin-top:20px}.toolbar{display:flex;gap:10px;align-items:center;justify-content:space-between;flex-wrap:wrap;margin:0 0 22px}.toolbar .tabs{margin:0;flex-wrap:wrap}.toolbar input,.toolbar select,.field input,select{border:1px solid var(--line);border-radius:7px;padding:10px 12px;background:var(--panel);color:var(--text);font:inherit;font-size:12px}.toolbar select{max-width:100%}.field{display:flex;flex-direction:column;gap:8px;font-size:12px;margin-bottom:18px}.field input{width:100%;background:var(--bg)}.date-fields{display:grid;grid-template-columns:1fr 1fr;gap:16px}.metrics-row{display:flex;gap:24px;padding:18px 0;margin-bottom:10px;color:var(--muted);font-size:11px}.metrics-row strong{font-family:Georgia;font-size:28px;color:var(--text);margin-right:6px;font-weight:400}.goals-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}.goal-card{padding:25px;position:relative;overflow:hidden}.goal-card:before{content:'';position:absolute;inset:0 auto 0 0;width:3px;background:var(--green)}.goal-card h3{font-size:19px;margin:17px 0 8px;word-break:break-word}.goal-card .section-head{margin-bottom:0}.goal-date{font-size:11px;color:var(--muted)}.cover-stack{display:flex;gap:0;margin:24px 0 20px;height:92px;padding-left:4px}.mini-cover{width:66px;height:92px;padding:10px 8px;border-radius:2px 4px 4px 2px;box-shadow:4px 5px 10px #0002;display:flex;flex-direction:column;flex-shrink:0;position:relative;overflow:hidden;border-left:3px solid #0002}.cover-stack .mini-cover{margin-left:-4px;transform:rotate(-3deg);transform-origin:bottom}.cover-stack .mini-cover:nth-child(2n){transform:rotate(4deg)}.mini-cover b{font-family:'Songti SC',serif;font-size:12px;position:relative;z-index:1;line-height:1.5}.mini-cover small{font-size:6px;position:relative;margin-top:auto}.stack-more{align-self:center;margin-left:18px;color:var(--muted);font-size:12px}.goal-progress{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--muted);margin:12px 0 9px}.goal-progress strong{font-size:22px;color:var(--text);font-family:Georgia;font-weight:400}.goal-card .progress{height:5px;margin:0 0 19px}.goal-card .progress i{background:var(--green)}.goal-card-footer{display:flex;align-items:center;justify-content:space-between;border-top:1px solid var(--line);padding-top:16px}.goal-card-footer small{font-size:10px;color:var(--muted)}.goal-card-footer button{font-size:11px;color:var(--accent)}.goal-intro{display:grid;grid-template-columns:1fr auto;gap:28px;background:var(--quote);padding:26px 30px;margin-bottom:24px;border:1px solid var(--line);border-radius:12px}.goal-intro h2{font-family:'Songti SC',serif;font-size:23px;font-weight:500;margin:0 0 10px}.goal-intro p{max-width:500px}.goal-intro .symbol{font-family:Georgia;font-size:65px;align-self:center;color:#819583}.goal-summary-row{display:block;text-align:left;width:100%;padding:16px 0;border-top:1px solid var(--line)}.goal-summary-row strong{font-size:12px;font-weight:500}.goal-summary-row small{display:block;margin-top:7px;color:var(--muted);font-size:10px}.goal-summary-row .progress{margin-top:10px}.list-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px}.list-card{padding:21px;display:block;text-align:left;transition:transform .2s}.list-card:hover{transform:translateY(-3px)}.list-art{height:117px;display:flex;align-items:center;justify-content:center;background:var(--quote);border-radius:7px;margin-bottom:18px}.list-art .mini-cover:nth-child(2){transform:translateY(-5px);margin:0 -4px;z-index:1}.list-card h3{font-size:15px;margin-bottom:8px}.list-card p{font-size:11px}.list-card>small{display:block;color:var(--muted);font-size:10px;margin-top:15px}.full-books{grid-template-columns:repeat(5,minmax(0,1fr));gap:26px}.full-books .cover{height:168px;width:118px}.row-list{display:flex;flex-direction:column}.book-row{display:flex;align-items:center;gap:15px;padding:14px 0;border-bottom:1px solid var(--line)}.book-row:last-child{border:0}.book-row .mini-cover{width:42px;height:58px;padding:5px}.book-row .mini-cover b{font-size:8px}.book-row .mini-cover small{display:none}.book-row-text{flex:1;min-width:0}.book-row-text b{font-size:12px;font-weight:500}.book-row-text p{font-size:10pLine truncated
@media(max-width:1200px){.full-books{grid-template-columns:repeat(4,minmax(0,1fr))}.list-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.insight-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:900px){.goals-grid{grid-template-columns:1fr}.columns>.rightcol{display:flex}.notebook-grid{grid-template-columns:1fr}.notebook-menu{display:flex;overflow:auto}.notebook-menu button{min-width:160px}.full-books{grid-template-columns:repeat(3,minmax(0,1fr))}.insight-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.sidebar .nav{gap:9px}.welcome{align-items:flex-start;gap:15px}.welcome h1{line-height:1.4}.welcome .primary{white-space:nowrap}.goal-intro{padding:22px}.demo-banner span:last-child{display:none}}@media(max-width:600px){.full-books{grid-template-columns:repeat(2,minmax(0,1fr))}.list-grid,.insight-grid{grid-template-columns:1fr}.goal-intro{grid-template-columns:1fr}.goal-intro .symbol{display:none}.goal-card{padding:20px}.welcome{flex-direction:column}.welcome .page-actions{align-self:flex-end}.panel{padding:18px}.date-fields{grid-template-columns:1fr;gap:0}.detail-body>.cover{width:95px;flex-shrink:0}.detail-body{gap:15px}.detail-info h3{font-size:18px}.book-details-grid{grid-template-columns:1fr}.toolbar input{width:100%}.metrics-row{gap:16px;flex-wrap:wrap}.metrics-row strong{font-size:23px}.goal-intro h2{font-size:20px}.cover-stack .mini-cover{width:57px}.stack-more{font-size:10px;margin-left:10px}.demo-banner{font-size:9px}.goal-card h3{font-size:17px}dialog{padding:20px}.editor-footer{gap:10px}.book-row{gap:9px}.book-row-text b{font-size:11px}}
.compact-value{font-family:Georgia,'PingFang SC',sans-serif;font-size:25px;line-height:1.5;letter-spacing:-.5px}.stat-foot{line-height:1.7}.insight-detail .insight-grid{margin-top:0}.sidebar .nav button[aria-current=page]{color:var(--accent)}
@media(max-width:600px){.compact-value{font-size:20px}.list-art .mini-cover{width:56px}.full-books .cover{width:105px;height:159px}.goal-card .section-head{gap:10px}.book-row .mini-cover{display:none}.detail-tabs{gap:16px}.date-fields input{min-height:40px}.detail-info .primary{font-size:11px;padding:9px}}
.mini-cover.c2,.mini-cover.c3{color:#fff5df}
.export-preview{width:100%;height:320px;margin-top:20px;border:1px solid var(--line);border-radius:8px;padding:18px;background:var(--bg);color:var(--text);font:12px/1.9 'PingFang SC',sans-serif;resize:vertical}
+635 -1238
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);
+13 -208
View File
@@ -1,211 +1,16 @@
<!DOCTYPE html> <!doctype html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="light">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>阅间 · 微信读书看板</title>
<title>我的读书数据</title> <link rel="stylesheet" href="/static/css/styles.css">
<link rel="stylesheet" href="/static/css/style.css">
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
<!-- 防闪烁:在 CSS 加载后、页面渲染前设置主题 -->
<script>
(function() {
var saved = localStorage.getItem('theme');
if (saved) {
document.documentElement.setAttribute('data-theme', saved);
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
</head> </head>
<body> <body>
<!-- 数据库连接告警横幅(MySQL 连接失败时显示) --> <svg style="display:none" aria-hidden="true"><defs>
<div id="dbAlertBanner" class="db-alert-banner" style="display:none;"> <symbol id="book" viewBox="0 0 24 24"><path d="M3 4c4-1 6 0 9 2 3-2 5-3 9-2v15c-4-1-6 0-9 2-3-2-5-3-9-2zM12 6v15"/></symbol><symbol id="grid" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></symbol><symbol id="note" viewBox="0 0 24 24"><path d="M13 4H5v16h14v-8M11 13l1-4 7-7 3 3-7 7z"/></symbol><symbol id="search-icon" viewBox="0 0 24 24"><circle cx="10" cy="10" r="6.5"/><path d="m15 15 6 6"/></symbol><symbol id="chart" viewBox="0 0 24 24"><path d="M4 3v17h17M8 15v-4m5 4V7m5 8V4"/></symbol><symbol id="clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 6v6l4 2"/></symbol><symbol id="moon" viewBox="0 0 24 24"><path d="M20 14A9 9 0 0 1 10 3a9 9 0 1 0 10 11Z"/></symbol><symbol id="sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1 1m12 12 1 1M5 19l1-1M18 6l1-1"/></symbol><symbol id="sync" viewBox="0 0 24 24"><path d="M20 7v5h-5M4 17v-5h5M6 6a8 8 0 0 1 14 6M4 12a8 8 0 0 0 14 6"/></symbol><symbol id="flag" viewBox="0 0 24 24"><path d="M5 21V3c5-4 9 4 14 0v11c-5 4-9-4-14 0"/></symbol>
<span class="db-alert-icon">⚠️</span> </defs></svg>
<span class="db-alert-text">MySQL 数据库连接失败,数据可能无法正常加载。请检查 MySQL 服务是否已启动。</span> <aside class="sidebar"><div class="logo"><div class="logo-mark"><svg><use href="#book"/></svg></div><div>阅间<small>READING SPACE</small></div></div><div class="navlabel">我的阅读空间</div><nav class="nav" aria-label="主导航"><button class="active" data-page="overview" title="阅读概览"><svg><use href="#grid"/></svg><span>阅读概览</span></button><button data-page="shelf" title="我的书架"><svg><use href="#book"/></svg><span>我的书架</span><em>8</em></button><button data-page="goals" title="阅读目标"><svg><use href="#flag"/></svg><span>阅读目标</span></button><button data-page="notes" title="笔记与划线"><svg><use href="#note"/></svg><span>笔记与划线</span></button><button data-page="discover" title="发现好书"><svg><use href="#search-icon"/></svg><span>发现好书</span></button></nav><div class="sidebar-bottom"><div class="connection"><span class="dot"></span>微信读书 · 个人空间<small>让每一页,都留下痕迹</small></div><div class="profile"><div class="avatar">读</div><div>我的阅读空间<small>数据来自你的微信读书账号</small></div></div></div></aside>
<span class="db-alert-detail" id="dbAlertDetail"></span> <main class="main"><header class="topbar"><div class="breadcrumb">我的空间 <span>/ &nbsp; <b id="crumb" style="font-weight:400">阅读概览</b></span></div><div class="top-actions"><label class="searchbox"><svg><use href="#search-icon"/></svg><input id="global-search" placeholder="搜索书籍、书单…" aria-label="全局搜索"><span class="key">⌘ K</span></label><button class="iconbtn" id="theme" aria-label="切换至暗色主题"><svg><use href="#moon"/></svg></button><div class="avatar">L</div></div></header><div class="demo-banner"><span><span class="dot"></span><span id="conn-state">正在连接微信读书…</span></span><span>阅读目标保存在当前浏览器</span></div><div id="page-content"></div><footer class="footer"><span>阅间 · 与好书相伴,与自己相遇</span><span id="footer-state">数据来自微信读书个人账号</span></footer></main>
</div> <dialog id="detail" aria-label="内容详情"><div class="dialog-top"><h2 id="detail-title">书籍详情</h2><button class="iconbtn" data-action="close-detail" aria-label="关闭详情">✕</button></div><div id="detail-content"></div></dialog>
<dialog id="goal-editor" aria-label="编辑阅读目标"><form id="goal-form"><div class="dialog-top"><div><div class="eyebrow">MAKE ROOM FOR READING</div><h2 id="editor-title">新建阅读目标</h2></div><button type="button" class="iconbtn" data-action="close-editor" aria-label="关闭目标编辑">✕</button></div><label class="field">目标名称<input id="goal-name" maxlength="60" placeholder="例如:秋季文学计划" required></label><div class="date-fields"><label class="field">开始日期<input id="goal-start" type="date" required></label><label class="field">结束日期<input id="goal-end" type="date" required></label></div><div class="section-head picker-head"><h3>想读些什么?</h3><span id="selection-count" class="pill"></span></div><p class="hint">选择书籍或加入书单当前书目,重复书籍只算一本,已读完直接计入完成。</p><label class="field"><input id="picker-search" placeholder="筛选书名、作者或书单…" aria-label="筛选目标书目"></label><div id="goal-picker"></div><p id="goal-error" class="form-error" role="alert"></p><div class="editor-footer"><span>仅保存在此浏览器</span><button class="primary" type="submit">保存阅读目标</button></div></form></dialog>
<!-- 导航栏 --> <div id="toast" class="hidden" role="status"></div><script src="/static/js/weapi.js"></script><script src="/static/js/app.js"></script></body></html>
<nav class="navbar">
<div class="nav-container">
<div class="nav-logo" onclick="switchTab('overview')" style="cursor:pointer">
<span class="logo-icon">📖</span>
<span class="logo-text">我的读书数据</span>
</div>
<div class="nav-links">
<a class="nav-link active" data-tab="overview" onclick="switchTab('overview')">总览</a>
<a class="nav-link" data-tab="charts" onclick="switchTab('charts')">数据图表</a>
<a class="nav-link" data-tab="top-books" onclick="switchTab('top-books')">读书排行</a>
<a class="nav-link" data-tab="shelf" onclick="switchTab('shelf')">书架</a>
<a class="nav-link" data-tab="notes" onclick="switchTab('notes')">笔记</a>
<a class="nav-link" data-tab="discover" onclick="switchTab('discover')">推荐</a>
</div>
<div class="nav-right">
<span class="nav-update" id="fetchTime"></span>
<button class="theme-toggle" onclick="toggleTheme()" title="切换明亮/黑暗模式" aria-label="切换主题">
<span class="icon-sun">☀️</span>
<span class="icon-moon">🌙</span>
</button>
</div>
</div>
</nav>
<!-- 顶部常驻搜索框 -->
<div class="global-search-bar">
<div class="container">
<div class="global-search-wrapper">
<span class="global-search-icon">🔍</span>
<input type="text" id="globalSearchInput" class="global-search-input" placeholder="搜索图书..." onkeydown="if(event.key==='Enter')globalSearch()">
<button class="global-search-btn" onclick="globalSearch()">搜索</button>
</div>
</div>
</div>
<!-- ===== 主视图(标签页切换) ===== -->
<main id="mainView">
<!-- 总览标签页 -->
<section id="tab-overview" class="tab-panel active">
<div class="hero-section">
<div class="hero-bg"></div>
<div class="hero-content">
<h1 class="hero-title">阅读,是一场漫长的旅行</h1>
<p class="hero-subtitle" id="heroSubtitle">正在加载数据...</p>
<div class="hero-stats" id="heroStats"></div>
</div>
</div>
<section class="stats-cards-section">
<div class="container">
<div class="stats-grid" id="statsGrid"></div>
</div>
</section>
</section>
<!-- 数据图表标签页 -->
<section id="tab-charts" class="tab-panel">
<div class="container" style="padding-top:40px;padding-bottom:60px">
<h2 class="section-title">📊 阅读数据可视化</h2>
<div class="charts-grid">
<div class="chart-card large">
<h3 class="chart-title">年度阅读时长趋势</h3>
<div id="chart-yearly" class="chart-container"></div>
</div>
<div class="chart-card">
<h3 class="chart-title">24小时阅读时段分布</h3>
<div id="chart-hours" class="chart-container"></div>
</div>
<div class="chart-card">
<h3 class="chart-title">偏好阅读分类</h3>
<div id="chart-categories" class="chart-container"></div>
</div>
<div class="chart-card large">
<h3 class="chart-title">偏好作者 TOP 10</h3>
<div id="chart-authors" class="chart-container"></div>
</div>
</div>
</div>
</section>
<!-- 读书排行标签页 -->
<section id="tab-top-books" class="tab-panel">
<div class="container" style="padding-top:40px;padding-bottom:60px">
<h2 class="section-title">🏆 阅读时长排行</h2>
<div class="top-books-list" id="topBooksGrid"></div>
</div>
</section>
<!-- 书架标签页 -->
<section id="tab-shelf" class="tab-panel">
<div class="container" style="padding-top:40px;padding-bottom:60px">
<h2 class="section-title">📚 我的书架</h2>
<div class="shelf-controls">
<div class="shelf-filters">
<input type="text" id="shelfSearch" class="shelf-search" placeholder="搜索书名、作者...">
<label class="filter-checkbox">
<input type="checkbox" id="finishedOnly"> 仅看已读完
</label>
<button class="shelf-refresh-btn" id="shelfRefreshBtn" onclick="refreshShelf()" title="重新加载书架数据">
<span class="btn-icon">⟳</span>
<span class="btn-text">刷新</span>
</button>
</div>
<div class="shelf-info" id="shelfInfo"></div>
</div>
<div class="shelf-categories" id="shelfCategories"></div>
<div class="book-grid" id="bookGrid"></div>
<div class="pagination" id="pagination"></div>
</div>
</section>
<!-- 笔记标签页 -->
<section id="tab-notes" class="tab-panel">
<div class="container" style="padding-top:40px;padding-bottom:60px">
<h2 class="section-title">✍️ 笔记概览</h2>
<div class="notes-summary" id="notesSummary"></div>
<div class="notebooks-grid" id="notebooksGrid"></div>
</div>
</section>
<!-- 推荐标签页 -->
<section id="tab-discover" class="tab-panel">
<div class="container" style="padding-top:40px;padding-bottom:60px">
<h2 class="section-title">🔍 推荐发现</h2>
<div class="discover-tabs">
<button class="discover-tab active" id="tabRecommend" onclick="switchDiscoverTab('recommend')">为你推荐</button>
<button class="discover-tab" id="tabSearch" onclick="switchDiscoverTab('search')">搜索书籍</button>
</div>
<div id="recommendPanel" class="discover-panel">
<div class="discover-controls">
<button class="btn-primary" onclick="loadRecommend(0)">刷新推荐</button>
<button class="btn-secondary" onclick="loadMoreRecommend()">换一批</button>
</div>
<div class="discover-grid" id="recommendGrid"></div>
</div>
<div id="searchPanel" class="discover-panel" style="display:none">
<div class="search-box">
<input type="text" id="searchInput" class="search-input" placeholder="输入书名、作者或关键词..." onkeydown="if(event.key==='Enter')doSearch()">
<button class="btn-primary" onclick="doSearch()">搜索</button>
</div>
<div class="discover-grid" id="searchGrid"></div>
</div>
</div>
</section>
</main>
<!-- ===== 想法详情独立页面 ===== -->
<div id="thoughtsPage" class="thoughts-page" style="display:none">
<div class="thoughts-page-header">
<div class="container">
<button class="back-btn" onclick="closeThoughtsPage()">← 返回</button>
<div class="thoughts-book-header" id="thoughtsBookHeader"></div>
</div>
</div>
<div class="container thoughts-content" id="thoughtsContent">
<div class="loading"><div class="loading-spinner"></div></div>
</div>
</div>
<!-- 书籍详情弹窗 -->
<div class="modal-overlay" id="bookDetailModal" onclick="closeModal('bookDetailModal')">
<div class="modal-content book-detail-modal" onclick="event.stopPropagation()">
<span class="modal-close" onclick="closeModal('bookDetailModal')">&times;</span>
<div id="bookDetailBody" class="modal-body"></div>
</div>
</div>
<!-- 回到顶部按钮 -->
<button id="backToTop" class="back-to-top" onclick="scrollToTop()" title="回到顶部" aria-label="回到顶部">↑</button>
<!-- 页脚 -->
<footer class="footer">
<div class="container">
<p>数据来源:微信读书 · 数据存储:本地 MySQL · 更新时间:<span id="footerTime"></span></p>
<p class="footer-hint">点击书籍封面可在微信读书 App 中打开</p>
</div>
</footer>
<script src="/static/js/app.js"></script>
</body>
</html>