mirror of
https://github.com/KingHWJ/WeReadBoard.git
synced 2026-11-03 18:24:58 +08:00
feat: add book detail, notes detail, recommendation page and MySQL schema
新增功能: - 书籍详情弹窗:点击书架书籍展示详细信息(书名、作者、出版社、ISBN、字数、评分、简介、阅读进度),数据实时从微信读书 API 获取并入库 - 笔记详情弹窗:点击有笔记的书展示划线和想法/点评,按章节分组展示,数据实时获取并入库 - 推荐页面:个性化推荐(为你推荐)+ 关键词搜索书籍,数据从微信读书 API 获取并入库 - 修复 Flask 中文 URL 参数编码问题 后端新增 API: - GET /api/book/detail?book_id=xxx — 书籍详情 - GET /api/book/notes?book_id=xxx — 笔记详情(划线+想法) - GET /api/recommend?maxIdx=0 — 个性化推荐 - GET /api/search?keyword=xxx — 搜索书籍 数据库新增表: - book_details, book_progress, book_highlights, book_chapters - book_reviews, recommended_books, search_results MySQL 版本建表语句 schema_mysql.sql: - 18 张表的完整 DDL,含索引、字符集 utf8mb4、字段注释 - 适用于后续从 SQLite 迁移至 MySQL
This commit is contained in:
1 parent
1e9c537b86
commit
ebfa3a1852
6 files changed
+1660
-4
No files matched your search
@@ -8,6 +8,7 @@ import json
|
||||
import os
|
||||
import sqlite3
|
||||
import time
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
|
||||
from flask import Flask, jsonify, render_template, request
|
||||
@@ -16,6 +17,80 @@ app = Flask(__name__, static_folder="static", template_folder="templates")
|
||||
|
||||
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "weread.db")
|
||||
|
||||
WEREAD_API_URL = "https://i.weread.qq.com/api/agent/gateway"
|
||||
WEREAD_API_KEY = os.environ.get("WEREAD_API_KEY", "")
|
||||
WEREAD_SKILL_VERSION = "1.0.3"
|
||||
|
||||
|
||||
def weread_api_call(api_name, **kwargs):
|
||||
"""调用微信读书 API(按需获取数据时使用)"""
|
||||
body = {"api_name": api_name, "skill_version": WEREAD_SKILL_VERSION}
|
||||
body.update(kwargs)
|
||||
req = urllib.request.Request(
|
||||
WEREAD_API_URL,
|
||||
data=json.dumps(body).encode("utf-8"),
|
||||
headers={
|
||||
"Authorization": f"Bearer {WEREAD_API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
|
||||
|
||||
def ensure_tables(conn):
|
||||
"""确保按需数据表存在"""
|
||||
c = conn.cursor()
|
||||
c.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS book_details (
|
||||
book_id TEXT PRIMARY KEY,
|
||||
title TEXT, author TEXT, translator TEXT, cover TEXT,
|
||||
intro TEXT, category TEXT, publisher TEXT, publish_time TEXT,
|
||||
isbn TEXT, word_count INTEGER, new_rating REAL,
|
||||
new_rating_count INTEGER, fetched_at INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS book_progress (
|
||||
book_id TEXT PRIMARY KEY,
|
||||
progress INTEGER, chapter_uid INTEGER, chapter_offset INTEGER,
|
||||
update_time INTEGER, record_reading_time INTEGER,
|
||||
finish_time INTEGER, is_start_reading INTEGER, fetched_at INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS book_highlights (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
book_id TEXT, bookmark_id TEXT, chapter_uid INTEGER,
|
||||
mark_text TEXT, create_time INTEGER, type INTEGER,
|
||||
range_text TEXT, color_style INTEGER, fetched_at INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS book_chapters (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
book_id TEXT, chapter_uid INTEGER, chapter_idx INTEGER,
|
||||
title TEXT, UNIQUE(book_id, chapter_uid)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS book_reviews (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
book_id TEXT, review_id TEXT, content TEXT,
|
||||
create_time INTEGER, star INTEGER, chapter_name TEXT,
|
||||
is_finish INTEGER, fetched_at INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS recommended_books (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
book_id TEXT, title TEXT, author TEXT, cover TEXT,
|
||||
intro TEXT, category TEXT, reason TEXT,
|
||||
reading_count INTEGER, new_rating REAL,
|
||||
new_rating_count INTEGER, rating_title TEXT, fetched_at INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS search_results (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
keyword TEXT, book_id TEXT, title TEXT, author TEXT,
|
||||
cover TEXT, intro TEXT, publisher TEXT, category TEXT,
|
||||
reading_count INTEGER, new_rating REAL,
|
||||
new_rating_count INTEGER, rating_title TEXT,
|
||||
search_idx INTEGER, fetched_at INTEGER
|
||||
);
|
||||
""")
|
||||
conn.commit()
|
||||
|
||||
|
||||
def get_db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
@@ -371,5 +446,405 @@ def api_archives():
|
||||
return jsonify(archives)
|
||||
|
||||
|
||||
# ===== 按需获取:书籍详情 =====
|
||||
|
||||
@app.route("/api/book/detail")
|
||||
def api_book_detail():
|
||||
"""书籍详情:实时调微信读书 API 获取,入库后返回"""
|
||||
book_id = request.args.get("book_id", "")
|
||||
if not book_id:
|
||||
return jsonify({"error": "book_id is required"}), 400
|
||||
|
||||
conn = get_db()
|
||||
ensure_tables(conn)
|
||||
c = conn.cursor()
|
||||
now = int(time.time())
|
||||
|
||||
# 1. 获取书籍信息
|
||||
try:
|
||||
info = weread_api_call("/book/info", bookId=book_id)
|
||||
c.execute(
|
||||
"""INSERT OR REPLACE INTO book_details
|
||||
(book_id, title, author, translator, cover, intro, category,
|
||||
publisher, publish_time, isbn, word_count, new_rating,
|
||||
new_rating_count, fetched_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
book_id,
|
||||
info.get("title", ""),
|
||||
info.get("author", ""),
|
||||
info.get("translator", ""),
|
||||
info.get("cover", ""),
|
||||
info.get("intro", ""),
|
||||
info.get("category", ""),
|
||||
info.get("publisher", ""),
|
||||
info.get("publishTime", ""),
|
||||
info.get("isbn", ""),
|
||||
info.get("wordCount", 0),
|
||||
info.get("newRating", 0),
|
||||
info.get("newRatingCount", 0),
|
||||
now,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
# API 调用失败时尝试从数据库读取缓存
|
||||
c.execute("SELECT * FROM book_details WHERE book_id=?", (book_id,))
|
||||
cached = c.fetchone()
|
||||
if not cached:
|
||||
return jsonify({"error": f"获取书籍信息失败: {str(e)}"}), 500
|
||||
|
||||
# 2. 获取阅读进度
|
||||
progress_data = {}
|
||||
try:
|
||||
prog = weread_api_call("/book/getprogress", bookId=book_id)
|
||||
book_prog = prog.get("book", {})
|
||||
c.execute(
|
||||
"""INSERT OR REPLACE INTO book_progress
|
||||
(book_id, progress, chapter_uid, chapter_offset, update_time,
|
||||
record_reading_time, finish_time, is_start_reading, fetched_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
book_id,
|
||||
book_prog.get("progress", 0),
|
||||
book_prog.get("chapterUid", 0),
|
||||
book_prog.get("chapterOffset", 0),
|
||||
book_prog.get("updateTime", 0),
|
||||
book_prog.get("recordReadingTime", 0),
|
||||
book_prog.get("finishTime", 0),
|
||||
book_prog.get("isStartReading", 0),
|
||||
now,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
progress_data = {
|
||||
"progress": book_prog.get("progress", 0),
|
||||
"record_reading_time": book_prog.get("recordReadingTime", 0),
|
||||
"record_reading_time_str": format_duration(book_prog.get("recordReadingTime", 0)),
|
||||
"update_time": ts_to_date(book_prog.get("updateTime", 0)),
|
||||
"finish_time": ts_to_date(book_prog.get("finishTime", 0)),
|
||||
"is_start_reading": book_prog.get("isStartReading", 0),
|
||||
}
|
||||
except Exception:
|
||||
c.execute("SELECT * FROM book_progress WHERE book_id=?", (book_id,))
|
||||
cached = c.fetchone()
|
||||
if cached:
|
||||
progress_data = {
|
||||
"progress": cached["progress"],
|
||||
"record_reading_time": cached["record_reading_time"],
|
||||
"record_reading_time_str": format_duration(cached["record_reading_time"]),
|
||||
"update_time": ts_to_date(cached["update_time"]),
|
||||
"finish_time": ts_to_date(cached["finish_time"]),
|
||||
"is_start_reading": cached["is_start_reading"],
|
||||
}
|
||||
|
||||
# 3. 组装返回
|
||||
c.execute("SELECT * FROM book_details WHERE book_id=?", (book_id,))
|
||||
detail = c.fetchone()
|
||||
conn.close()
|
||||
|
||||
if not detail:
|
||||
return jsonify({"error": "数据获取失败"}), 500
|
||||
|
||||
return jsonify({
|
||||
"book_id": book_id,
|
||||
"title": detail["title"],
|
||||
"author": detail["author"],
|
||||
"translator": detail["translator"],
|
||||
"cover": detail["cover"],
|
||||
"intro": detail["intro"],
|
||||
"category": detail["category"],
|
||||
"publisher": detail["publisher"],
|
||||
"publish_time": detail["publish_time"],
|
||||
"isbn": detail["isbn"],
|
||||
"word_count": detail["word_count"],
|
||||
"new_rating": detail["new_rating"],
|
||||
"new_rating_count": detail["new_rating_count"],
|
||||
"progress": progress_data,
|
||||
"deep_link": f"weread://reading?bId={book_id}",
|
||||
})
|
||||
|
||||
|
||||
# ===== 按需获取:笔记详情 =====
|
||||
|
||||
@app.route("/api/book/notes")
|
||||
def api_book_notes():
|
||||
"""笔记详情:实时调微信读书 API 获取划线+想法,入库后返回"""
|
||||
book_id = request.args.get("book_id", "")
|
||||
if not book_id:
|
||||
return jsonify({"error": "book_id is required"}), 400
|
||||
|
||||
conn = get_db()
|
||||
ensure_tables(conn)
|
||||
c = conn.cursor()
|
||||
now = int(time.time())
|
||||
|
||||
# 1. 获取划线
|
||||
highlights = []
|
||||
chapters = {}
|
||||
try:
|
||||
data = weread_api_call("/book/bookmarklist", bookId=book_id)
|
||||
# 保存章节信息
|
||||
for ch in data.get("chapters", []):
|
||||
c.execute(
|
||||
"""INSERT OR REPLACE INTO book_chapters
|
||||
(book_id, chapter_uid, chapter_idx, title)
|
||||
VALUES (?,?,?,?)""",
|
||||
(book_id, ch.get("chapterUid", 0), ch.get("chapterIdx", 0), ch.get("title", "")),
|
||||
)
|
||||
chapters[ch.get("chapterUid", 0)] = ch.get("title", "")
|
||||
|
||||
# 保存划线
|
||||
c.execute("DELETE FROM book_highlights WHERE book_id=?", (book_id,))
|
||||
for bm in data.get("updated", []):
|
||||
c.execute(
|
||||
"""INSERT INTO book_highlights
|
||||
(book_id, bookmark_id, chapter_uid, mark_text, create_time,
|
||||
type, range_text, color_style, fetched_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
book_id,
|
||||
bm.get("bookmarkId", ""),
|
||||
bm.get("chapterUid", 0),
|
||||
bm.get("markText", ""),
|
||||
bm.get("createTime", 0),
|
||||
bm.get("type", 0),
|
||||
bm.get("range", ""),
|
||||
bm.get("colorStyle", 0),
|
||||
now,
|
||||
),
|
||||
)
|
||||
highlights.append({
|
||||
"bookmark_id": bm.get("bookmarkId", ""),
|
||||
"chapter_uid": bm.get("chapterUid", 0),
|
||||
"chapter_title": chapters.get(bm.get("chapterUid", 0), ""),
|
||||
"mark_text": bm.get("markText", ""),
|
||||
"create_time": ts_to_date(bm.get("createTime", 0)),
|
||||
"range": bm.get("range", ""),
|
||||
})
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
# 从数据库读取缓存
|
||||
c.execute("SELECT * FROM book_highlights WHERE book_id=? ORDER BY create_time", (book_id,))
|
||||
for row in c.fetchall():
|
||||
c2 = conn.cursor()
|
||||
c2.execute("SELECT title FROM book_chapters WHERE book_id=? AND chapter_uid=?", (book_id, row["chapter_uid"]))
|
||||
ch_row = c2.fetchone()
|
||||
highlights.append({
|
||||
"bookmark_id": row["bookmark_id"],
|
||||
"chapter_uid": row["chapter_uid"],
|
||||
"chapter_title": ch_row["title"] if ch_row else "",
|
||||
"mark_text": row["mark_text"],
|
||||
"create_time": ts_to_date(row["create_time"]),
|
||||
"range": row["range_text"],
|
||||
})
|
||||
|
||||
# 2. 获取想法/点评
|
||||
reviews = []
|
||||
try:
|
||||
rev_data = weread_api_call("/review/list/mine", bookid=book_id, count=100)
|
||||
c.execute("DELETE FROM book_reviews WHERE book_id=?", (book_id,))
|
||||
for item in rev_data.get("reviews", []):
|
||||
rev = item.get("review", {})
|
||||
c.execute(
|
||||
"""INSERT INTO book_reviews
|
||||
(book_id, review_id, content, create_time, star,
|
||||
chapter_name, is_finish, fetched_at)
|
||||
VALUES (?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
book_id,
|
||||
rev.get("reviewId", ""),
|
||||
rev.get("content", ""),
|
||||
rev.get("createTime", 0),
|
||||
rev.get("star", 0),
|
||||
rev.get("chapterName", ""),
|
||||
rev.get("isFinish", 0),
|
||||
now,
|
||||
),
|
||||
)
|
||||
reviews.append({
|
||||
"review_id": rev.get("reviewId", ""),
|
||||
"content": rev.get("content", ""),
|
||||
"create_time": ts_to_date(rev.get("createTime", 0)),
|
||||
"star": rev.get("star", 0),
|
||||
"chapter_name": rev.get("chapterName", ""),
|
||||
"is_finish": rev.get("isFinish", 0),
|
||||
})
|
||||
conn.commit()
|
||||
except Exception:
|
||||
c.execute("SELECT * FROM book_reviews WHERE book_id=? ORDER BY create_time DESC", (book_id,))
|
||||
for row in c.fetchall():
|
||||
reviews.append({
|
||||
"review_id": row["review_id"],
|
||||
"content": row["content"],
|
||||
"create_time": ts_to_date(row["create_time"]),
|
||||
"star": row["star"],
|
||||
"chapter_name": row["chapter_name"],
|
||||
"is_finish": row["is_finish"],
|
||||
})
|
||||
|
||||
conn.close()
|
||||
|
||||
# 按章节分组划线
|
||||
grouped_highlights = {}
|
||||
for h in highlights:
|
||||
ch_title = h["chapter_title"] or f"章节{h['chapter_uid']}"
|
||||
if ch_title not in grouped_highlights:
|
||||
grouped_highlights[ch_title] = []
|
||||
grouped_highlights[ch_title].append(h)
|
||||
|
||||
return jsonify({
|
||||
"book_id": book_id,
|
||||
"highlights": highlights,
|
||||
"highlight_count": len(highlights),
|
||||
"grouped_highlights": grouped_highlights,
|
||||
"reviews": reviews,
|
||||
"review_count": len(reviews),
|
||||
})
|
||||
|
||||
|
||||
# ===== 推荐书籍 =====
|
||||
|
||||
@app.route("/api/recommend")
|
||||
def api_recommend():
|
||||
"""个性化推荐:实时调微信读书 API,入库后返回"""
|
||||
max_idx = int(request.args.get("maxIdx", 0))
|
||||
conn = get_db()
|
||||
ensure_tables(conn)
|
||||
c = conn.cursor()
|
||||
now = int(time.time())
|
||||
|
||||
try:
|
||||
data = weread_api_call("/book/recommend", count=12, maxIdx=max_idx)
|
||||
books = data.get("books", [])
|
||||
for book in books:
|
||||
rating_detail = book.get("newRatingDetail", {})
|
||||
c.execute(
|
||||
"""INSERT OR REPLACE INTO recommended_books
|
||||
(book_id, title, author, cover, intro, category, reason,
|
||||
reading_count, new_rating, new_rating_count, rating_title, fetched_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
book.get("bookId", ""),
|
||||
book.get("title", ""),
|
||||
book.get("author", ""),
|
||||
book.get("cover", ""),
|
||||
book.get("intro", ""),
|
||||
book.get("category", ""),
|
||||
book.get("reason", ""),
|
||||
book.get("readingCount", 0),
|
||||
book.get("newRating", 0),
|
||||
book.get("newRatingCount", 0),
|
||||
rating_detail.get("title", "") if isinstance(rating_detail, dict) else "",
|
||||
now,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"获取推荐失败: {str(e)}"}), 500
|
||||
|
||||
conn.close()
|
||||
|
||||
result = []
|
||||
for book in books:
|
||||
rating_detail = book.get("newRatingDetail", {})
|
||||
result.append({
|
||||
"book_id": book.get("bookId", ""),
|
||||
"title": book.get("title", ""),
|
||||
"author": book.get("author", ""),
|
||||
"cover": book.get("cover", ""),
|
||||
"intro": book.get("intro", ""),
|
||||
"category": book.get("category", ""),
|
||||
"reason": book.get("reason", ""),
|
||||
"reading_count": book.get("readingCount", 0),
|
||||
"new_rating": book.get("newRating", 0),
|
||||
"new_rating_count": book.get("newRatingCount", 0),
|
||||
"rating_title": rating_detail.get("title", "") if isinstance(rating_detail, dict) else "",
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
"books": result,
|
||||
"has_more": data.get("hasMore", 0),
|
||||
"last_search_idx": books[-1].get("searchIdx", 0) if books else 0,
|
||||
})
|
||||
|
||||
|
||||
# ===== 搜索书籍 =====
|
||||
|
||||
@app.route("/api/search")
|
||||
def api_search():
|
||||
"""搜索书籍:实时调微信读书 API,入库后返回"""
|
||||
keyword = request.args.get("keyword", "").strip()
|
||||
# 修复 Flask URL 参数中文编码问题
|
||||
try:
|
||||
keyword = keyword.encode("latin-1").decode("utf-8")
|
||||
except (UnicodeDecodeError, UnicodeEncodeError):
|
||||
pass
|
||||
if not keyword:
|
||||
return jsonify({"error": "keyword is required"}), 400
|
||||
|
||||
max_idx = int(request.args.get("maxIdx", 0))
|
||||
conn = get_db()
|
||||
ensure_tables(conn)
|
||||
c = conn.cursor()
|
||||
now = int(time.time())
|
||||
|
||||
try:
|
||||
data = weread_api_call("/store/search", keyword=keyword, scope=10, maxIdx=max_idx)
|
||||
results = data.get("results", [])
|
||||
books = []
|
||||
for group in results:
|
||||
for item in group.get("books", []):
|
||||
bi = item.get("bookInfo", {})
|
||||
rating_detail = item.get("newRatingDetail", {})
|
||||
c.execute(
|
||||
"""INSERT OR REPLACE INTO search_results
|
||||
(keyword, book_id, title, author, cover, intro, publisher,
|
||||
category, reading_count, new_rating, new_rating_count,
|
||||
rating_title, search_idx, fetched_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
keyword,
|
||||
bi.get("bookId", ""),
|
||||
bi.get("title", ""),
|
||||
bi.get("author", ""),
|
||||
bi.get("cover", ""),
|
||||
bi.get("intro", ""),
|
||||
bi.get("publisher", ""),
|
||||
bi.get("category", ""),
|
||||
item.get("readingCount", 0),
|
||||
item.get("newRating", 0),
|
||||
item.get("newRatingCount", 0),
|
||||
rating_detail.get("title", "") if isinstance(rating_detail, dict) else "",
|
||||
item.get("searchIdx", 0),
|
||||
now,
|
||||
),
|
||||
)
|
||||
books.append({
|
||||
"book_id": bi.get("bookId", ""),
|
||||
"title": bi.get("title", ""),
|
||||
"author": bi.get("author", ""),
|
||||
"cover": bi.get("cover", ""),
|
||||
"intro": bi.get("intro", ""),
|
||||
"publisher": bi.get("publisher", ""),
|
||||
"category": bi.get("category", ""),
|
||||
"reading_count": item.get("readingCount", 0),
|
||||
"new_rating": item.get("newRating", 0),
|
||||
"new_rating_count": item.get("newRatingCount", 0),
|
||||
"rating_title": rating_detail.get("title", "") if isinstance(rating_detail, dict) else "",
|
||||
})
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"搜索失败: {str(e)}"}), 500
|
||||
|
||||
conn.close()
|
||||
|
||||
return jsonify({
|
||||
"keyword": keyword,
|
||||
"books": books,
|
||||
"has_more": data.get("hasMore", 0),
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8080, debug=False)
|
||||
+120
@@ -180,6 +180,126 @@ def init_db(db_path):
|
||||
)
|
||||
""")
|
||||
|
||||
# ===== 按需获取的详细数据表 =====
|
||||
|
||||
# 书籍详细信息(来自 /book/info)
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS book_details (
|
||||
book_id TEXT PRIMARY KEY,
|
||||
title TEXT,
|
||||
author TEXT,
|
||||
translator TEXT,
|
||||
cover TEXT,
|
||||
intro TEXT,
|
||||
category TEXT,
|
||||
publisher TEXT,
|
||||
publish_time TEXT,
|
||||
isbn TEXT,
|
||||
word_count INTEGER,
|
||||
new_rating REAL,
|
||||
new_rating_count INTEGER,
|
||||
fetched_at INTEGER
|
||||
)
|
||||
""")
|
||||
|
||||
# 阅读进度(来自 /book/getprogress)
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS book_progress (
|
||||
book_id TEXT PRIMARY KEY,
|
||||
progress INTEGER,
|
||||
chapter_uid INTEGER,
|
||||
chapter_offset INTEGER,
|
||||
update_time INTEGER,
|
||||
record_reading_time INTEGER,
|
||||
finish_time INTEGER,
|
||||
is_start_reading INTEGER,
|
||||
fetched_at INTEGER
|
||||
)
|
||||
""")
|
||||
|
||||
# 划线内容(来自 /book/bookmarklist)
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS book_highlights (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
book_id TEXT,
|
||||
bookmark_id TEXT,
|
||||
chapter_uid INTEGER,
|
||||
mark_text TEXT,
|
||||
create_time INTEGER,
|
||||
type INTEGER,
|
||||
range_text TEXT,
|
||||
color_style INTEGER,
|
||||
fetched_at INTEGER
|
||||
)
|
||||
""")
|
||||
|
||||
# 章节信息(来自 /book/bookmarklist 的 chapters)
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS book_chapters (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
book_id TEXT,
|
||||
chapter_uid INTEGER,
|
||||
chapter_idx INTEGER,
|
||||
title TEXT,
|
||||
UNIQUE(book_id, chapter_uid)
|
||||
)
|
||||
""")
|
||||
|
||||
# 个人想法/点评(来自 /review/list/mine)
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS book_reviews (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
book_id TEXT,
|
||||
review_id TEXT,
|
||||
content TEXT,
|
||||
create_time INTEGER,
|
||||
star INTEGER,
|
||||
chapter_name TEXT,
|
||||
is_finish INTEGER,
|
||||
fetched_at INTEGER
|
||||
)
|
||||
""")
|
||||
|
||||
# 推荐书籍(来自 /book/recommend)
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS recommended_books (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
book_id TEXT,
|
||||
title TEXT,
|
||||
author TEXT,
|
||||
cover TEXT,
|
||||
intro TEXT,
|
||||
category TEXT,
|
||||
reason TEXT,
|
||||
reading_count INTEGER,
|
||||
new_rating REAL,
|
||||
new_rating_count INTEGER,
|
||||
rating_title TEXT,
|
||||
fetched_at INTEGER
|
||||
)
|
||||
""")
|
||||
|
||||
# 搜索结果(来自 /store/search)
|
||||
c.execute("""
|
||||
CREATE TABLE IF NOT EXISTS search_results (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
keyword TEXT,
|
||||
book_id TEXT,
|
||||
title TEXT,
|
||||
author TEXT,
|
||||
cover TEXT,
|
||||
intro TEXT,
|
||||
publisher TEXT,
|
||||
category TEXT,
|
||||
reading_count INTEGER,
|
||||
new_rating REAL,
|
||||
new_rating_count INTEGER,
|
||||
rating_title TEXT,
|
||||
search_idx INTEGER,
|
||||
fetched_at INTEGER
|
||||
)
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
return conn
|
||||
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
-- ============================================================
|
||||
-- 微信读书数据展示网站 - MySQL 版本建表语句
|
||||
-- 适用于后续从 SQLite 迁移至 MySQL
|
||||
-- 字符集: utf8mb4 (支持完整 Unicode,包括 emoji)
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS `weread_dashboard`
|
||||
DEFAULT CHARACTER SET utf8mb4
|
||||
DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE `weread_dashboard`;
|
||||
|
||||
-- ============================================================
|
||||
-- 1. 书架书籍表
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `books` (
|
||||
`book_id` VARCHAR(64) NOT NULL COMMENT '书籍唯一标识',
|
||||
`title` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '书名',
|
||||
`author` VARCHAR(255) DEFAULT NULL COMMENT '作者',
|
||||
`cover` VARCHAR(512) DEFAULT NULL COMMENT '封面图 URL',
|
||||
`category` VARCHAR(128) DEFAULT NULL COMMENT '分类',
|
||||
`finish_reading` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否读完 (1=读完)',
|
||||
`read_update_time` INT UNSIGNED DEFAULT NULL COMMENT '最近阅读时间 (Unix 时间戳)',
|
||||
`secret` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否私密 (1=私密)',
|
||||
`is_top` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否置顶 (1=置顶)',
|
||||
`update_time` INT UNSIGNED DEFAULT NULL COMMENT '书籍更新时间 (Unix 时间戳)',
|
||||
`deep_link` VARCHAR(512) DEFAULT NULL COMMENT '深度链接',
|
||||
PRIMARY KEY (`book_id`),
|
||||
INDEX `idx_books_category` (`category`),
|
||||
INDEX `idx_books_finish_reading` (`finish_reading`),
|
||||
INDEX `idx_books_read_update_time` (`read_update_time` DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='书架书籍信息';
|
||||
|
||||
-- ============================================================
|
||||
-- 2. 阅读统计概要表
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `reading_stats` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`mode` VARCHAR(20) NOT NULL COMMENT '统计维度: overall/annually/monthly/weekly',
|
||||
`period` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '周期标识',
|
||||
`total_read_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '总阅读时长 (秒)',
|
||||
`read_days` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '有效阅读天数',
|
||||
`day_average_read_time` DOUBLE DEFAULT 0 COMMENT '日均阅读时长 (秒)',
|
||||
`regist_time` INT UNSIGNED DEFAULT NULL COMMENT '用户注册时间 (Unix 时间戳)',
|
||||
`raw_json` LONGTEXT DEFAULT NULL COMMENT '原始 API 返回 JSON',
|
||||
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_reading_stats_mode` (`mode`, `period`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='阅读统计概要';
|
||||
|
||||
-- ============================================================
|
||||
-- 3. 阅读时长时间序列(按年/月/日分桶)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `read_times` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`mode` VARCHAR(30) NOT NULL COMMENT '统计维度 (含_daily后缀)',
|
||||
`period` VARCHAR(20) NOT NULL DEFAULT '',
|
||||
`bucket_timestamp` INT UNSIGNED NOT NULL COMMENT '分桶起始时间戳',
|
||||
`duration` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读时长 (秒)',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_read_times_mode_period` (`mode`, `period`),
|
||||
INDEX `idx_read_times_bucket` (`bucket_timestamp`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='阅读时长时间序列';
|
||||
|
||||
-- ============================================================
|
||||
-- 4. 阅读时长最久的书籍排行
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `read_longest` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`mode` VARCHAR(20) NOT NULL,
|
||||
`period` VARCHAR(20) NOT NULL DEFAULT '',
|
||||
`book_id` VARCHAR(64) NOT NULL,
|
||||
`title` VARCHAR(255) DEFAULT NULL,
|
||||
`author` VARCHAR(255) DEFAULT NULL,
|
||||
`cover` VARCHAR(512) DEFAULT NULL,
|
||||
`read_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读时长 (秒)',
|
||||
`tags` VARCHAR(512) DEFAULT NULL COMMENT '标签 JSON 数组',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_read_longest_mode` (`mode`, `period`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='阅读时长排行';
|
||||
|
||||
-- ============================================================
|
||||
-- 5. 偏好分类
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `prefer_categories` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`mode` VARCHAR(20) NOT NULL,
|
||||
`period` VARCHAR(20) NOT NULL DEFAULT '',
|
||||
`category_id` INT UNSIGNED DEFAULT NULL,
|
||||
`category_title` VARCHAR(128) DEFAULT NULL,
|
||||
`parent_category_title` VARCHAR(128) DEFAULT NULL,
|
||||
`val` DOUBLE DEFAULT 0 COMMENT '偏好权重',
|
||||
`reading_time` INT UNSIGNED DEFAULT 0 COMMENT '该分类阅读时长 (秒)',
|
||||
`reading_count` INT UNSIGNED DEFAULT 0 COMMENT '该分类阅读本数',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_prefer_cat_mode` (`mode`, `period`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='偏好阅读分类';
|
||||
|
||||
-- ============================================================
|
||||
-- 6. 偏好作者
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `prefer_authors` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`mode` VARCHAR(20) NOT NULL,
|
||||
`period` VARCHAR(20) NOT NULL DEFAULT '',
|
||||
`author_id` INT UNSIGNED DEFAULT NULL,
|
||||
`name` VARCHAR(255) DEFAULT NULL,
|
||||
`count` INT UNSIGNED DEFAULT 0 COMMENT '阅读该作者的书本数',
|
||||
`read_time` VARCHAR(64) DEFAULT NULL COMMENT '阅读时长 (格式化字符串)',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_prefer_author_mode` (`mode`, `period`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='偏好作者';
|
||||
|
||||
-- ============================================================
|
||||
-- 7. 24小时阅读时段分布
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `prefer_time` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`mode` VARCHAR(20) NOT NULL,
|
||||
`period` VARCHAR(20) NOT NULL DEFAULT '',
|
||||
`hour_index` TINYINT UNSIGNED NOT NULL COMMENT '小时 (0-23)',
|
||||
`duration` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读时长 (秒)',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_prefer_time_mode` (`mode`, `period`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='24小时阅读时段分布';
|
||||
|
||||
-- ============================================================
|
||||
-- 8. 笔记本概览
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `notebooks` (
|
||||
`book_id` VARCHAR(64) NOT NULL,
|
||||
`title` VARCHAR(255) DEFAULT NULL,
|
||||
`author` VARCHAR(255) DEFAULT NULL,
|
||||
`cover` VARCHAR(512) DEFAULT NULL,
|
||||
`review_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '想法/点评数',
|
||||
`note_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '划线数',
|
||||
`bookmark_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '书签数',
|
||||
`reading_progress` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读进度 (0-100)',
|
||||
`marked_status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '标记状态 (1=读完)',
|
||||
`sort` BIGINT DEFAULT NULL COMMENT '排序值',
|
||||
PRIMARY KEY (`book_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='笔记本概览';
|
||||
|
||||
-- ============================================================
|
||||
-- 9. 阅读统计摘要项
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `read_stat_items` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`mode` VARCHAR(20) NOT NULL,
|
||||
`period` VARCHAR(20) NOT NULL DEFAULT '',
|
||||
`stat_name` VARCHAR(64) DEFAULT NULL COMMENT '统计项名称 (读过/读完/阅读/笔记)',
|
||||
`counts` VARCHAR(64) DEFAULT NULL COMMENT '统计值文案',
|
||||
`scheme` VARCHAR(512) DEFAULT NULL COMMENT 'App 跳转链接',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_read_stat_items_mode` (`mode`, `period`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='阅读统计摘要项';
|
||||
|
||||
-- ============================================================
|
||||
-- 10. 书单/归档
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `archives` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(255) DEFAULT NULL COMMENT '书单名称',
|
||||
`book_ids` LONGTEXT DEFAULT NULL COMMENT 'bookId 列表 JSON 数组',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='书单/归档';
|
||||
|
||||
-- ============================================================
|
||||
-- 11. 数据采集元信息
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `fetch_meta` (
|
||||
`key` VARCHAR(64) NOT NULL,
|
||||
`value` TEXT DEFAULT NULL,
|
||||
PRIMARY KEY (`key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='数据采集元信息';
|
||||
|
||||
-- ============================================================
|
||||
-- 12. 书籍详细信息(按需获取,来自 /book/info)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `book_details` (
|
||||
`book_id` VARCHAR(64) NOT NULL,
|
||||
`title` VARCHAR(255) DEFAULT NULL,
|
||||
`author` VARCHAR(255) DEFAULT NULL,
|
||||
`translator` VARCHAR(255) DEFAULT NULL COMMENT '译者',
|
||||
`cover` VARCHAR(512) DEFAULT NULL,
|
||||
`intro` LONGTEXT DEFAULT NULL COMMENT '简介',
|
||||
`category` VARCHAR(128) DEFAULT NULL,
|
||||
`publisher` VARCHAR(255) DEFAULT NULL,
|
||||
`publish_time` VARCHAR(32) DEFAULT NULL COMMENT '出版时间',
|
||||
`isbn` VARCHAR(32) DEFAULT NULL,
|
||||
`word_count` INT UNSIGNED DEFAULT 0 COMMENT '总字数',
|
||||
`new_rating` DECIMAL(5,1) DEFAULT 0 COMMENT '评分 (百分制)',
|
||||
`new_rating_count` INT UNSIGNED DEFAULT 0 COMMENT '评分人数',
|
||||
`fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '获取时间',
|
||||
PRIMARY KEY (`book_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='书籍详细信息';
|
||||
|
||||
-- ============================================================
|
||||
-- 13. 阅读进度(按需获取,来自 /book/getprogress)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `book_progress` (
|
||||
`book_id` VARCHAR(64) NOT NULL,
|
||||
`progress` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读进度百分比 (0-100)',
|
||||
`chapter_uid` INT UNSIGNED DEFAULT NULL COMMENT '当前阅读章节 UID',
|
||||
`chapter_offset` INT UNSIGNED DEFAULT NULL COMMENT '当前章节内偏移',
|
||||
`update_time` INT UNSIGNED DEFAULT NULL COMMENT '最后阅读时间 (Unix 时间戳)',
|
||||
`record_reading_time` INT UNSIGNED DEFAULT 0 COMMENT '累计阅读时长 (秒)',
|
||||
`finish_time` INT UNSIGNED DEFAULT NULL COMMENT '读完时间 (Unix 时间戳)',
|
||||
`is_start_reading` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已开始阅读',
|
||||
`fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`book_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='阅读进度';
|
||||
|
||||
-- ============================================================
|
||||
-- 14. 划线内容(按需获取,来自 /book/bookmarklist)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `book_highlights` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`book_id` VARCHAR(64) NOT NULL,
|
||||
`bookmark_id` VARCHAR(64) DEFAULT NULL COMMENT '划线唯一 ID',
|
||||
`chapter_uid` INT UNSIGNED DEFAULT NULL COMMENT '所在章节 UID',
|
||||
`mark_text` LONGTEXT DEFAULT NULL COMMENT '划线原文',
|
||||
`create_time` INT UNSIGNED DEFAULT NULL COMMENT '创建时间 (Unix 时间戳)',
|
||||
`type` TINYINT DEFAULT NULL COMMENT '类型',
|
||||
`range_text` VARCHAR(64) DEFAULT NULL COMMENT '位置范围',
|
||||
`color_style` TINYINT DEFAULT NULL COMMENT '划线颜色样式',
|
||||
`fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_highlights_book_id` (`book_id`),
|
||||
INDEX `idx_highlights_chapter` (`book_id`, `chapter_uid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='划线内容';
|
||||
|
||||
-- ============================================================
|
||||
-- 15. 章节信息(来自 /book/bookmarklist 的 chapters)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `book_chapters` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`book_id` VARCHAR(64) NOT NULL,
|
||||
`chapter_uid` INT UNSIGNED NOT NULL,
|
||||
`chapter_idx` INT UNSIGNED DEFAULT NULL COMMENT '章节序号',
|
||||
`title` VARCHAR(255) DEFAULT NULL COMMENT '章节标题',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_book_chapter` (`book_id`, `chapter_uid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='章节信息';
|
||||
|
||||
-- ============================================================
|
||||
-- 16. 个人想法/点评(按需获取,来自 /review/list/mine)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `book_reviews` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`book_id` VARCHAR(64) NOT NULL,
|
||||
`review_id` VARCHAR(64) DEFAULT NULL COMMENT '想法唯一 ID',
|
||||
`content` LONGTEXT DEFAULT NULL COMMENT '内容文本',
|
||||
`create_time` INT UNSIGNED DEFAULT NULL COMMENT '创建时间 (Unix 时间戳)',
|
||||
`star` TINYINT DEFAULT -1 COMMENT '评分 (0-5, -1=无评分)',
|
||||
`chapter_name` VARCHAR(255) DEFAULT NULL COMMENT '所在章节名',
|
||||
`is_finish` TINYINT(1) DEFAULT 0 COMMENT '是否书评',
|
||||
`fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_reviews_book_id` (`book_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='个人想法/点评';
|
||||
|
||||
-- ============================================================
|
||||
-- 17. 推荐书籍(按需获取,来自 /book/recommend)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `recommended_books` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`book_id` VARCHAR(64) NOT NULL,
|
||||
`title` VARCHAR(255) DEFAULT NULL,
|
||||
`author` VARCHAR(255) DEFAULT NULL,
|
||||
`cover` VARCHAR(512) DEFAULT NULL,
|
||||
`intro` TEXT DEFAULT NULL,
|
||||
`category` VARCHAR(128) DEFAULT NULL,
|
||||
`reason` VARCHAR(512) DEFAULT NULL COMMENT '推荐理由',
|
||||
`reading_count` INT UNSIGNED DEFAULT 0 COMMENT '在读人数',
|
||||
`new_rating` DECIMAL(5,1) DEFAULT 0 COMMENT '评分 (百分制)',
|
||||
`new_rating_count` INT UNSIGNED DEFAULT 0 COMMENT '评分人数',
|
||||
`rating_title` VARCHAR(32) DEFAULT NULL COMMENT '评分标签 (神作/力荐等)',
|
||||
`fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_recommended_book_id` (`book_id`),
|
||||
INDEX `idx_recommended_fetched` (`fetched_at` DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='推荐书籍';
|
||||
|
||||
-- ============================================================
|
||||
-- 18. 搜索结果(按需获取,来自 /store/search)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS `search_results` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`keyword` VARCHAR(255) NOT NULL COMMENT '搜索关键词',
|
||||
`book_id` VARCHAR(64) NOT NULL,
|
||||
`title` VARCHAR(255) DEFAULT NULL,
|
||||
`author` VARCHAR(255) DEFAULT NULL,
|
||||
`cover` VARCHAR(512) DEFAULT NULL,
|
||||
`intro` TEXT DEFAULT NULL,
|
||||
`publisher` VARCHAR(255) DEFAULT NULL,
|
||||
`category` VARCHAR(128) DEFAULT NULL,
|
||||
`reading_count` INT UNSIGNED DEFAULT 0,
|
||||
`new_rating` DECIMAL(5,1) DEFAULT 0,
|
||||
`new_rating_count` INT UNSIGNED DEFAULT 0,
|
||||
`rating_title` VARCHAR(32) DEFAULT NULL,
|
||||
`search_idx` INT UNSIGNED DEFAULT 0 COMMENT '搜索序号',
|
||||
`fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_search_keyword` (`keyword`),
|
||||
INDEX `idx_search_book_id` (`book_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='搜索结果';
|
||||
@@ -678,6 +678,454 @@ body {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ===== 推荐发现区 ===== */
|
||||
.discover-section {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.discover-tabs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.discover-tab {
|
||||
padding: 8px 24px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
background: var(--bg-card);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.discover-tab.active {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.discover-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 8px 20px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-dark);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 8px 20px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-card);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
max-width: 500px;
|
||||
margin: 0 auto 24px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
background: var(--bg-card);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(193, 123, 74, 0.15);
|
||||
}
|
||||
|
||||
.discover-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.discover-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.discover-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-hover);
|
||||
}
|
||||
|
||||
.discover-cover {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
object-fit: cover;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.discover-info {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.discover-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.discover-author {
|
||||
font-size: 12px;
|
||||
color: var(--text-light);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.discover-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.discover-rating {
|
||||
font-size: 13px;
|
||||
color: #e65100;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.discover-rating-label {
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: #fff3e0;
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
.discover-reading-count {
|
||||
font-size: 11px;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.discover-reason {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--accent);
|
||||
background: rgba(193, 123, 74, 0.08);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.discover-category {
|
||||
font-size: 11px;
|
||||
color: var(--text-light);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ===== 弹窗通用 ===== */
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 40px 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius);
|
||||
max-width: 700px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
font-size: 28px;
|
||||
color: var(--text-light);
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
/* ===== 书籍详情弹窗 ===== */
|
||||
.book-detail-modal .detail-header {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.book-detail-modal .detail-cover {
|
||||
width: 120px;
|
||||
height: 160px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
box-shadow: var(--shadow);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.book-detail-modal .detail-meta {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.book-detail-modal .detail-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.book-detail-modal .detail-author {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.book-detail-modal .detail-tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.book-detail-modal .detail-tag {
|
||||
font-size: 12px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
background: rgba(193, 123, 74, 0.1);
|
||||
color: var(--accent-dark);
|
||||
}
|
||||
|
||||
.book-detail-modal .detail-progress-info {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.book-detail-modal .detail-progress-bar {
|
||||
height: 8px;
|
||||
background: var(--border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.book-detail-modal .detail-progress-fill {
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.book-detail-modal .detail-intro {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.8;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.book-detail-modal .detail-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px 16px;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.book-detail-modal .detail-info-item {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.book-detail-modal .detail-info-item strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.book-detail-modal .detail-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* ===== 笔记详情弹窗 ===== */
|
||||
.notes-detail-modal {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.notes-detail-modal .notes-header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.notes-detail-modal .notes-cover {
|
||||
width: 60px;
|
||||
height: 80px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.notes-detail-modal .notes-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.notes-detail-modal .notes-author {
|
||||
font-size: 13px;
|
||||
color: var(--text-light);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.notes-detail-modal .notes-counts {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.notes-detail-modal .notes-counts strong {
|
||||
color: var(--accent-dark);
|
||||
}
|
||||
|
||||
.notes-detail-modal .notes-section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 20px 0 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.notes-detail-modal .chapter-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.notes-detail-modal .chapter-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-dark);
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 0;
|
||||
border-left: 3px solid var(--accent);
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.notes-detail-modal .highlight-item {
|
||||
padding: 10px 14px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.7;
|
||||
border-left: 3px solid var(--accent-light);
|
||||
}
|
||||
|
||||
.notes-detail-modal .highlight-date {
|
||||
font-size: 11px;
|
||||
color: var(--text-light);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.notes-detail-modal .review-item {
|
||||
padding: 10px 14px;
|
||||
background: #fff8e1;
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.7;
|
||||
border-left: 3px solid #ffc107;
|
||||
}
|
||||
|
||||
.notes-detail-modal .review-meta {
|
||||
font-size: 11px;
|
||||
color: var(--text-light);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.notes-detail-modal .review-star {
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
.notes-detail-modal .empty-state {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--text-light);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* ===== 响应式 ===== */
|
||||
@media (max-width: 768px) {
|
||||
.nav-links {
|
||||
|
||||
+248
-4
@@ -24,6 +24,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
loadTopBooks();
|
||||
loadShelf(1);
|
||||
loadNotebooks();
|
||||
loadRecommend(0);
|
||||
initNavScroll();
|
||||
});
|
||||
|
||||
@@ -525,7 +526,7 @@ async function loadNotebooks() {
|
||||
.map((nb) => {
|
||||
const progress = nb.reading_progress || 0;
|
||||
return `
|
||||
<div class="notebook-card">
|
||||
<div class="notebook-card" onclick="openNotes('${nb.book_id}', '${nb.title.replace(/'/g, "\\'")}', '${(nb.author || '').replace(/'/g, "\\'")}', '${nb.cover}')" style="cursor:pointer">
|
||||
<img class="notebook-cover" src="${nb.cover}" alt="${nb.title}"
|
||||
onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2260%22 height=%2280%22%3E%3Crect fill=%22%23e8e0d4%22 width=%2260%22 height=%2280%22/%3E%3C/svg%3E'">
|
||||
<div class="notebook-info">
|
||||
@@ -548,9 +549,252 @@ async function loadNotebooks() {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 打开书籍 =====
|
||||
// ===== 打开书籍详情弹窗 =====
|
||||
function openBook(bookId) {
|
||||
if (bookId) {
|
||||
window.open(`weread://reading?bId=${bookId}`, "_blank");
|
||||
if (!bookId) return;
|
||||
showModal("bookDetailModal");
|
||||
const body = document.getElementById("bookDetailBody");
|
||||
body.innerHTML = `<div class="loading"><div class="loading-spinner"></div></div>`;
|
||||
|
||||
fetchAPI(`/api/book/detail?book_id=${bookId}`)
|
||||
.then((data) => {
|
||||
const p = data.progress || {};
|
||||
const progress = p.progress || 0;
|
||||
const rating = data.new_rating > 0 ? (data.new_rating / 10).toFixed(1) : null;
|
||||
const tags = [];
|
||||
if (data.category) tags.push(data.category);
|
||||
if (rating) tags.push(`${rating}分`);
|
||||
if (data.new_rating_count) tags.push(`${data.new_rating_count}人评分`);
|
||||
|
||||
body.innerHTML = `
|
||||
<div class="detail-header">
|
||||
<img class="detail-cover" src="${data.cover || ''}" alt="${data.title}"
|
||||
onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22120%22 height=%22160%22%3E%3Crect fill=%22%23e8e0d4%22 width=%22120%22 height=%22160%22/%3E%3C/svg%3E'">
|
||||
<div class="detail-meta">
|
||||
<div class="detail-title">${data.title}</div>
|
||||
<div class="detail-author">${data.author || ''}${data.translator ? ' / 译: ' + data.translator : ''}</div>
|
||||
<div class="detail-tags">${tags.map(t => `<span class="detail-tag">${t}</span>`).join('')}</div>
|
||||
${progress.is_start_reading ? `
|
||||
<div class="detail-progress-info">
|
||||
<div style="font-size:13px;color:var(--text-secondary)">
|
||||
阅读进度: <strong>${progress.progress}%</strong>
|
||||
${progress.record_reading_time_str ? ' · 累计 ' + progress.record_reading_time_str : ''}
|
||||
${progress.finish_time ? ' · 读完于 ' + progress.finish_time : ''}
|
||||
</div>
|
||||
<div class="detail-progress-bar">
|
||||
<div class="detail-progress-fill" style="width:${progress}%"></div>
|
||||
</div>
|
||||
</div>` : '<div style="font-size:13px;color:var(--text-light);margin-top:8px">尚未开始阅读</div>'}
|
||||
</div>
|
||||
</div>
|
||||
${data.intro ? `<div class="detail-intro">${data.intro}</div>` : ''}
|
||||
<div class="detail-info-grid">
|
||||
${data.publisher ? `<div class="detail-info-item"><strong>出版社:</strong> ${data.publisher}</div>` : ''}
|
||||
${data.publish_time ? `<div class="detail-info-item"><strong>出版时间:</strong> ${data.publish_time}</div>` : ''}
|
||||
${data.isbn ? `<div class="detail-info-item"><strong>ISBN:</strong> ${data.isbn}</div>` : ''}
|
||||
${data.word_count ? `<div class="detail-info-item"><strong>字数:</strong> ${(data.word_count / 10000).toFixed(1)}万字</div>` : ''}
|
||||
</div>
|
||||
<div class="detail-actions">
|
||||
<button class="btn-primary" onclick="window.open('weread://reading?bId=${bookId}','_blank')">在微信读书中打开</button>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.catch((e) => {
|
||||
body.innerHTML = `<div class="empty-state">加载失败: ${e.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 打开笔记详情弹窗 =====
|
||||
function openNotes(bookId, title, author, cover) {
|
||||
if (!bookId) return;
|
||||
showModal("notesDetailModal");
|
||||
const body = document.getElementById("notesDetailBody");
|
||||
body.innerHTML = `
|
||||
<div class="notes-header">
|
||||
<img class="notes-cover" src="${cover || ''}" alt="${title}"
|
||||
onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2260%22 height=%2280%22%3E%3Crect fill=%22%23e8e0d4%22 width=%2260%22 height=%2280%22/%3E%3C/svg%3E'">
|
||||
<div>
|
||||
<div class="notes-title">${title}</div>
|
||||
<div class="notes-author">${author || ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading"><div class="loading-spinner"></div></div>
|
||||
`;
|
||||
|
||||
fetchAPI(`/api/book/notes?book_id=${bookId}`)
|
||||
.then((data) => {
|
||||
let html = `
|
||||
<div class="notes-header">
|
||||
<img class="notes-cover" src="${cover || ''}" alt="${title}"
|
||||
onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2260%22 height=%2280%22%3E%3Crect fill=%22%23e8e0d4%22 width=%2260%22 height=%2280%22/%3E%3C/svg%3E'">
|
||||
<div>
|
||||
<div class="notes-title">${title}</div>
|
||||
<div class="notes-author">${author || ''}</div>
|
||||
<div class="notes-counts">
|
||||
<span>划线 <strong>${data.highlight_count}</strong></span>
|
||||
<span>想法/点评 <strong>${data.review_count}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 划线区
|
||||
if (data.highlight_count > 0) {
|
||||
html += `<div class="notes-section-title">📌 划线 (${data.highlight_count})</div>`;
|
||||
const grouped = data.grouped_highlights;
|
||||
for (const [chTitle, items] of Object.entries(grouped)) {
|
||||
html += `<div class="chapter-group">`;
|
||||
html += `<div class="chapter-title">${chTitle}</div>`;
|
||||
items.forEach((h) => {
|
||||
html += `
|
||||
<div class="highlight-item">
|
||||
${escapeHtml(h.mark_text)}
|
||||
<div class="highlight-date">${h.create_time || ''}</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += `</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 想法/点评区
|
||||
if (data.review_count > 0) {
|
||||
html += `<div class="notes-section-title">💭 想法/点评 (${data.review_count})</div>`;
|
||||
data.reviews.forEach((r) => {
|
||||
const stars = r.star > 0 ? `<span class="review-star">${"★".repeat(r.star)}${"☆".repeat(5 - r.star)}</span>` : "";
|
||||
html += `
|
||||
<div class="review-item">
|
||||
${escapeHtml(r.content)}
|
||||
<div class="review-meta">
|
||||
${stars}
|
||||
${r.chapter_name ? ' · ' + r.chapter_name : ''}
|
||||
${r.is_finish ? ' · 书评' : ''}
|
||||
· ${r.create_time || ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.highlight_count === 0 && data.review_count === 0) {
|
||||
html += `<div class="empty-state">暂无笔记数据</div>`;
|
||||
}
|
||||
|
||||
body.innerHTML = html;
|
||||
})
|
||||
.catch((e) => {
|
||||
body.innerHTML = `<div class="empty-state">加载失败: ${e.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 弹窗控制 =====
|
||||
function showModal(id) {
|
||||
document.getElementById(id).classList.add("active");
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
|
||||
function closeModal(id) {
|
||||
document.getElementById(id).classList.remove("active");
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
|
||||
// ESC 关闭弹窗
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
closeModal("bookDetailModal");
|
||||
closeModal("notesDetailModal");
|
||||
}
|
||||
});
|
||||
|
||||
// ===== HTML 转义 =====
|
||||
function escapeHtml(text) {
|
||||
if (!text) return "";
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// ===== 推荐发现 =====
|
||||
let recommendMaxIdx = 0;
|
||||
|
||||
function switchDiscoverTab(tab) {
|
||||
const recPanel = document.getElementById("recommendPanel");
|
||||
const searchPanel = document.getElementById("searchPanel");
|
||||
const tabRec = document.getElementById("tabRecommend");
|
||||
const tabSearch = document.getElementById("tabSearch");
|
||||
|
||||
if (tab === "recommend") {
|
||||
recPanel.style.display = "";
|
||||
searchPanel.style.display = "none";
|
||||
tabRec.classList.add("active");
|
||||
tabSearch.classList.remove("active");
|
||||
} else {
|
||||
recPanel.style.display = "none";
|
||||
searchPanel.style.display = "";
|
||||
tabRec.classList.remove("active");
|
||||
tabSearch.classList.add("active");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecommend(maxIdx) {
|
||||
const grid = document.getElementById("recommendGrid");
|
||||
grid.innerHTML = `<div class="loading"><div class="loading-spinner"></div></div>`;
|
||||
|
||||
try {
|
||||
const data = await fetchAPI(`/api/recommend?maxIdx=${maxIdx}`);
|
||||
recommendMaxIdx = data.last_search_idx || 0;
|
||||
renderDiscoverGrid(grid, data.books, true);
|
||||
} catch (e) {
|
||||
grid.innerHTML = `<div class="empty-state">加载推荐失败: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMoreRecommend() {
|
||||
loadRecommend(recommendMaxIdx);
|
||||
}
|
||||
|
||||
async function doSearch() {
|
||||
const keyword = document.getElementById("searchInput").value.trim();
|
||||
if (!keyword) return;
|
||||
|
||||
const grid = document.getElementById("searchGrid");
|
||||
grid.innerHTML = `<div class="loading"><div class="loading-spinner"></div></div>`;
|
||||
|
||||
try {
|
||||
const data = await fetchAPI(`/api/search?keyword=${encodeURIComponent(keyword)}`);
|
||||
if (data.books.length === 0) {
|
||||
grid.innerHTML = `<div class="empty-state">未找到相关书籍</div>`;
|
||||
return;
|
||||
}
|
||||
renderDiscoverGrid(grid, data.books, false);
|
||||
} catch (e) {
|
||||
grid.innerHTML = `<div class="empty-state">搜索失败: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderDiscoverGrid(grid, books, showReason) {
|
||||
grid.innerHTML = books
|
||||
.map((book) => {
|
||||
const rating = book.new_rating > 0 ? (book.new_rating / 10).toFixed(1) : null;
|
||||
const ratingLabel = book.rating_title ? `<span class="discover-rating-label">${book.rating_title}</span>` : "";
|
||||
const ratingHtml = rating ? `<span class="discover-rating">${rating}</span>` : "";
|
||||
const readingHtml = book.reading_count ? `<span class="discover-reading-count">${book.reading_count}人在读</span>` : "";
|
||||
const reasonHtml = (showReason && book.reason) ? `<div class="discover-reason">${book.reason}</div>` : "";
|
||||
const categoryHtml = book.category ? `<div class="discover-category">${book.category}</div>` : "";
|
||||
|
||||
return `
|
||||
<div class="discover-card" onclick="openBook('${book.book_id}')">
|
||||
<img class="discover-cover" src="${book.cover || ''}" alt="${book.title}"
|
||||
onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22200%22 height=%22180%22%3E%3Crect fill=%22%23e8e0d4%22 width=%22200%22 height=%22180%22/%3E%3Ctext x=%22100%22 y=%2290%22 text-anchor=%22middle%22 fill=%22%239b8e80%22 font-size=%2214%22%3E无封面%3C/text%3E%3C/svg%3E'">
|
||||
<div class="discover-info">
|
||||
<div class="discover-title">${book.title}</div>
|
||||
<div class="discover-author">${book.author || ''}</div>
|
||||
<div class="discover-meta">${ratingHtml}${ratingLabel}${readingHtml}</div>
|
||||
${categoryHtml}
|
||||
${reasonHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
<a href="#top-books" class="nav-link">读书排行</a>
|
||||
<a href="#shelf" class="nav-link">书架</a>
|
||||
<a href="#notes" class="nav-link">笔记</a>
|
||||
<a href="#discover" class="nav-link">推荐</a>
|
||||
</div>
|
||||
<div class="nav-update" id="fetchTime"></div>
|
||||
</div>
|
||||
@@ -117,6 +118,49 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 推荐发现 -->
|
||||
<section id="discover" class="discover-section">
|
||||
<div class="container">
|
||||
<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>
|
||||
|
||||
<!-- 书籍详情弹窗 -->
|
||||
<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')">×</span>
|
||||
<div id="bookDetailBody" class="modal-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 笔记详情弹窗 -->
|
||||
<div class="modal-overlay" id="notesDetailModal" onclick="closeModal('notesDetailModal')">
|
||||
<div class="modal-content notes-detail-modal" onclick="event.stopPropagation()">
|
||||
<span class="modal-close" onclick="closeModal('notesDetailModal')">×</span>
|
||||
<div id="notesDetailBody" class="modal-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
|
||||
Reference in new issue
Block a user