冯博深 8 godzin temu
commit
0a55296433
79 zmienionych plików z 6257 dodań i 0 usunięć
  1. 139 0
      README.md
  2. 34 0
      api/bookshelf.js
  3. 17 0
      api/category.js
  4. 29 0
      api/chapter.js
  5. 49 0
      api/chat.js
  6. 41 0
      api/content.js
  7. 26 0
      api/history.js
  8. 47 0
      api/user.js
  9. 20 0
      app.js
  10. 51 0
      app.json
  11. 10 0
      app.wxss
  12. 44 0
      images/README.md
  13. BIN
      images/bookshelf-active.png
  14. BIN
      images/bookshelf.png
  15. BIN
      images/home-active.png
  16. BIN
      images/home.png
  17. BIN
      images/profile-active.png
  18. BIN
      images/profile.png
  19. 223 0
      pages/audio/audio.js
  20. 4 0
      pages/audio/audio.json
  21. 66 0
      pages/audio/audio.wxml
  22. 287 0
      pages/audio/audio.wxss
  23. 72 0
      pages/bookshelf/bookshelf.js
  24. 3 0
      pages/bookshelf/bookshelf.json
  25. 41 0
      pages/bookshelf/bookshelf.wxml
  26. 201 0
      pages/bookshelf/bookshelf.wxss
  27. 372 0
      pages/chat/chat.js
  28. 6 0
      pages/chat/chat.json
  29. 63 0
      pages/chat/chat.wxml
  30. 183 0
      pages/chat/chat.wxss
  31. 225 0
      pages/detail/detail.js
  32. 3 0
      pages/detail/detail.json
  33. 98 0
      pages/detail/detail.wxml
  34. 391 0
      pages/detail/detail.wxss
  35. 113 0
      pages/history/history.js
  36. 3 0
      pages/history/history.json
  37. 31 0
      pages/history/history.wxml
  38. 132 0
      pages/history/history.wxss
  39. 200 0
      pages/index/index.js
  40. 6 0
      pages/index/index.json
  41. 164 0
      pages/index/index.wxml
  42. 546 0
      pages/index/index.wxss
  43. 94 0
      pages/login/login.js
  44. 4 0
      pages/login/login.json
  45. 42 0
      pages/login/login.wxml
  46. 86 0
      pages/login/login.wxss
  47. 18 0
      pages/logs/logs.js
  48. 4 0
      pages/logs/logs.json
  49. 6 0
      pages/logs/logs.wxml
  50. 16 0
      pages/logs/logs.wxss
  51. 102 0
      pages/profile/profile.js
  52. 3 0
      pages/profile/profile.json
  53. 45 0
      pages/profile/profile.wxml
  54. 233 0
      pages/profile/profile.wxss
  55. 159 0
      pages/read/read.js
  56. 3 0
      pages/read/read.json
  57. 85 0
      pages/read/read.wxml
  58. 238 0
      pages/read/read.wxss
  59. 115 0
      pages/register/register.js
  60. 3 0
      pages/register/register.json
  61. 77 0
      pages/register/register.wxml
  62. 68 0
      pages/register/register.wxss
  63. 85 0
      pages/settings/settings.js
  64. 9 0
      pages/settings/settings.json
  65. 50 0
      pages/settings/settings.wxml
  66. 114 0
      pages/settings/settings.wxss
  67. 41 0
      project.config.json
  68. 23 0
      project.private.config.json
  69. 7 0
      sitemap.json
  70. 125 0
      utils/api.js
  71. 47 0
      utils/user.js
  72. 19 0
      utils/util.js
  73. 72 0
      书籍封面说明.md
  74. 33 0
      关于基础库提示.md
  75. 25 0
      创建占位图标说明.md
  76. 78 0
      图标说明.md
  77. 103 0
      添加TabBar图标指南.md
  78. 28 0
      解决登录问题.md
  79. 57 0
      问题修复说明.md

+ 139 - 0
README.md

@@ -0,0 +1,139 @@
+# 听书阅读小程序
+
+## 项目简介
+
+这是听书阅读系统的微信小程序端,与后端共享数据库,提供电子书阅读和听书功能。
+
+## 功能模块
+
+### 用户模块
+- 用户登录/注册(普通用户)
+- 管理员登录(提示使用Web端)
+- 用户信息管理
+
+### 内容浏览
+- 首页:书籍列表、分类筛选、搜索
+- 书籍详情:查看简介、开始阅读/听书
+- 分类浏览:按分类查看内容
+
+### 阅读功能
+- 电子书阅读
+- 字体大小调节
+- 背景颜色设置(护眼模式)
+- 章节切换
+- 目录导航
+
+### 听书功能
+- 音频播放
+- 播放进度控制
+- 播放速度调节(0.5x - 2.0x)
+- 章节切换
+- 自动播放下一章
+
+### 个人中心
+- 用户信息展示
+- 我的书架
+- 阅读历史
+- 设置
+
+## 技术栈
+
+- 微信小程序原生开发
+- 小程序API调用
+- 本地存储(书架、历史记录)
+
+## 项目结构
+
+```
+xiao/
+├── api/              # API接口封装
+│   ├── user.js       # 用户相关API
+│   ├── content.js    # 内容相关API
+│   ├── category.js   # 分类相关API
+│   └── chapter.js    # 章节相关API
+├── utils/            # 工具类
+│   ├── api.js        # 网络请求封装
+│   └── user.js       # 用户工具函数
+├── pages/            # 页面
+│   ├── index/        # 首页
+│   ├── login/        # 登录页
+│   ├── register/     # 注册页
+│   ├── detail/       # 详情页
+│   ├── read/         # 阅读页
+│   ├── audio/        # 听书页
+│   ├── profile/      # 个人中心
+│   ├── bookshelf/    # 书架
+│   └── history/      # 历史记录
+├── app.js            # 小程序入口
+├── app.json          # 小程序配置
+└── app.wxss          # 全局样式
+```
+
+## 配置说明
+
+### 1. 修改API地址
+
+在 `utils/api.js` 中修改 `BASE_URL`:
+
+```javascript
+const BASE_URL = 'http://localhost:8080/api'; // 改为你的后端地址
+```
+
+**注意**:小程序要求使用HTTPS,本地开发可以使用微信开发者工具的不校验合法域名选项。
+
+### 2. 配置tabBar图标
+
+在 `app.json` 中配置了tabBar,需要准备以下图标(放在 `images/` 目录下):
+- `home.png` / `home-active.png` - 首页图标
+- `bookshelf.png` / `bookshelf-active.png` - 书架图标
+- `profile.png` / `profile-active.png` - 个人中心图标
+
+## 使用说明
+
+### 开发环境
+
+1. 使用微信开发者工具打开项目
+2. 在微信开发者工具中:
+   - 设置 -> 项目设置 -> 不校验合法域名(开发阶段)
+   - 确保后端服务已启动(默认 http://localhost:8080)
+
+### 登录说明
+
+- **普通用户**:使用 `/api/user/login` 接口登录
+- **管理员**:登录后会提示使用Web端管理后台
+
+### 功能说明
+
+1. **首页**:可以浏览书籍列表,支持分类筛选和搜索
+2. **详情页**:查看书籍详情,可以开始阅读或听书
+3. **阅读页**:支持字体大小和背景色调节
+4. **听书页**:支持播放速度调节和进度控制
+5. **个人中心**:查看用户信息、书架和历史记录
+
+## 注意事项
+
+1. **登录权限**:
+   - 浏览内容不需要登录
+   - 阅读/听书需要登录
+   - 管理员登录后会提示使用Web端
+
+2. **数据存储**:
+   - 用户信息:使用 `wx.setStorageSync` 存储
+   - 书架和历史:存储在本地,后续可对接后端API
+
+3. **网络请求**:
+   - 所有API请求都携带token(如果需要)
+   - token过期会自动跳转到登录页
+
+4. **图片资源**:
+   - 需要准备默认封面图片:`/images/default-cover.png`
+   - 需要准备默认头像:`/images/default-avatar.png`
+
+## 后续优化
+
+1. 对接后端书架和历史记录API
+2. 添加阅读进度同步
+3. 添加书签功能
+4. 添加评论功能
+5. 优化图片加载和缓存
+6. 添加离线下载功能

+ 34 - 0
api/bookshelf.js

@@ -0,0 +1,34 @@
+// 书架相关API
+const api = require('../utils/api');
+
+// 获取书架列表
+function getBookshelfList(userId) {
+  return api.get('/user/bookshelf/list', { userId });
+}
+
+// 加入书架
+function addToBookshelf(userId, contentId) {
+  return api.post('/user/bookshelf/add', {}, false, { userId, contentId });
+}
+
+// 移出书架
+function removeFromBookshelf(userId, contentId) {
+  return api.del('/user/bookshelf/remove', { userId, contentId });
+}
+
+// 检查是否在书架中
+function checkInBookshelf(userId, contentId) {
+  return api.get('/user/bookshelf/check', { userId, contentId });
+}
+
+module.exports = {
+  getBookshelfList,
+  addToBookshelf,
+  removeFromBookshelf,
+  checkInBookshelf
+};
+
+
+
+
+

+ 17 - 0
api/category.js

@@ -0,0 +1,17 @@
+// 分类相关API
+const api = require('../utils/api');
+
+// 获取分类列表
+function getCategoryList() {
+  return api.get('/app/category/list');
+}
+
+// 获取分类树
+function getCategoryTree() {
+  return api.get('/app/category/tree');
+}
+
+module.exports = {
+  getCategoryList,
+  getCategoryTree
+};

+ 29 - 0
api/chapter.js

@@ -0,0 +1,29 @@
+// 章节相关API
+const api = require('../utils/api');
+
+// 获取电子书章节列表
+function getBookChapterList(contentId) {
+  return api.get(`/app/book-chapter/list/${contentId}`);
+}
+
+// 获取电子书章节详情
+function getBookChapter(chapterId) {
+  return api.get(`/app/book-chapter/${chapterId}`);
+}
+
+// 获取听书章节列表
+function getAudioChapterList(contentId) {
+  return api.get(`/app/audio-chapter/list/${contentId}`);
+}
+
+// 获取听书章节详情
+function getAudioChapter(audioId) {
+  return api.get(`/app/audio-chapter/${audioId}`);
+}
+
+module.exports = {
+  getBookChapterList,
+  getBookChapter,
+  getAudioChapterList,
+  getAudioChapter
+};

+ 49 - 0
api/chat.js

@@ -0,0 +1,49 @@
+// 聊天相关API
+const api = require('../utils/api');
+
+// 创建会话
+function createSession(userId) {
+  return api.post('/chat/session', {}, true, { userId });
+}
+
+// 获取会话列表
+function getSessionList(userId) {
+  return api.get('/chat/sessions', { userId }, true);
+}
+
+// 获取会话详情
+function getSession(sessionId, userId) {
+  return api.get(`/chat/session/${sessionId}`, { userId }, true);
+}
+
+// 删除会话
+function deleteSession(sessionId, userId) {
+  return api.del(`/chat/session/${sessionId}`, { userId }, true);
+}
+
+// 发送消息
+function sendMessage(sessionId, message, userId) {
+  return api.post('/chat/message', {
+    sessionId: sessionId,
+    message: message
+  }, true, { userId });
+}
+
+// 获取消息历史
+function getMessageHistory(sessionId, userId, limit) {
+  const params = { userId };
+  if (limit) {
+    params.limit = limit;
+  }
+  return api.get(`/chat/messages/${sessionId}`, params, true);
+}
+
+module.exports = {
+  createSession,
+  getSessionList,
+  getSession,
+  deleteSession,
+  sendMessage,
+  getMessageHistory
+};
+

+ 41 - 0
api/content.js

@@ -0,0 +1,41 @@
+// 内容相关API
+const api = require('../utils/api');
+
+// 分页获取内容列表
+function getContentList(current = 1, size = 10, keyword = '', contentType = null, categoryId = null) {
+  const params = { current, size };
+  if (keyword && keyword.trim()) {
+    params.keyword = keyword;
+  }
+  // 只有当 contentType 不为 null 且不为 undefined 时才添加
+  if (contentType !== null && contentType !== undefined) {
+    params.contentType = contentType;
+  }
+  // 只有当 categoryId 不为 null 且不为 undefined 时才添加
+  if (categoryId !== null && categoryId !== undefined && categoryId !== 0) {
+    params.categoryId = categoryId;
+  }
+  return api.get('/app/content/page', params);
+}
+
+// 获取内容详情
+function getContentDetail(contentId) {
+  return api.get(`/app/content/${contentId}`);
+}
+
+// 增加内容浏览量
+function increaseViewCount(contentId) {
+  return api.post(`/content/${contentId}/view`, {}, false);
+}
+
+// 获取今日推荐内容
+function getRecommended(size = 6) {
+  return api.get('/app/content/recommended', { size }, false);
+}
+
+module.exports = {
+  getContentList,
+  getContentDetail,
+  increaseViewCount,
+  getRecommended
+};

+ 26 - 0
api/history.js

@@ -0,0 +1,26 @@
+// 阅读历史相关API
+const api = require('../utils/api');
+
+// 添加阅读历史
+function addHistory(contentId) {
+  return api.post('/app/history/add', { contentId }, true);
+}
+
+// 获取阅读历史列表
+function getHistoryList() {
+  return api.get('/app/history/list', {}, true);
+}
+
+// 清空阅读历史
+function clearHistory() {
+  return api.del('/app/history/clear', {}, true);
+}
+
+module.exports = {
+  addHistory,
+  getHistoryList,
+  clearHistory
+};
+
+
+

+ 47 - 0
api/user.js

@@ -0,0 +1,47 @@
+// 用户相关API
+const api = require('../utils/api');
+
+// 用户登录(普通用户)
+function userLogin(username, password) {
+  return api.post('/user/login', { username, password }, false);
+}
+
+// 管理员登录
+function adminLogin(username, password) {
+  return api.post('/admin/login', { username, password }, false);
+}
+
+// 用户注册(普通用户)
+function userRegister(userData) {
+  return api.post('/user/register', userData, false);
+}
+
+// 获取用户信息
+function getUserInfo(userId) {
+  return api.get(`/admin/user/${userId}`);
+}
+
+// 更新用户信息(管理员接口)
+function updateUserInfo(userData) {
+  return api.put('/admin/user', userData);
+}
+
+// 获取当前登录用户信息(小程序端)
+function getCurrentUserInfo() {
+  return api.get('/app/user/info');
+}
+
+// 更新当前登录用户信息(小程序端)
+function updateCurrentUserInfo(userData) {
+  return api.put('/app/user/update', userData);
+}
+
+module.exports = {
+  userLogin,
+  adminLogin,
+  userRegister,
+  getUserInfo,
+  updateUserInfo,
+  getCurrentUserInfo,
+  updateCurrentUserInfo
+};

+ 20 - 0
app.js

@@ -0,0 +1,20 @@
+// app.js
+const userUtil = require('./utils/user');
+
+App({
+  onLaunch() {
+    // 检查如果已登录的是管理员,清除登录信息
+    if (userUtil.isLogin()) {
+      const userInfo = userUtil.getUserInfo();
+      if (userInfo && userInfo.userRole === 1) {
+        userUtil.clearUserInfo();
+        console.log('检测到管理员账号,已清除登录信息');
+      }
+    }
+    // 不强制跳转登录,让各个页面自己判断
+    // 这样可以让用户先浏览,需要时再登录
+  },
+  globalData: {
+    userInfo: null
+  }
+})

+ 51 - 0
app.json

@@ -0,0 +1,51 @@
+{
+  "pages": [
+    "pages/index/index",
+    "pages/login/login",
+    "pages/register/register",
+    "pages/detail/detail",
+    "pages/read/read",
+    "pages/audio/audio",
+    "pages/profile/profile",
+    "pages/bookshelf/bookshelf",
+    "pages/history/history",
+    "pages/chat/chat",
+    "pages/settings/settings"
+  ],
+  "window": {
+    "navigationBarTextStyle": "black",
+    "navigationBarTitleText": "听书阅读",
+    "navigationBarBackgroundColor": "#ffffff",
+    "backgroundColor": "#f5f5f5"
+  },
+  "tabBar": {
+    "color": "#7A7E83",
+    "selectedColor": "#667eea",
+    "borderStyle": "black",
+    "backgroundColor": "#ffffff",
+    "list": [
+      {
+        "pagePath": "pages/index/index",
+        "iconPath": "images/home.png",
+        "selectedIconPath": "images/home-active.png",
+        "text": "首页"
+      },
+      {
+        "pagePath": "pages/bookshelf/bookshelf",
+        "iconPath": "images/bookshelf.png",
+        "selectedIconPath": "images/bookshelf-active.png",
+        "text": "书架"
+      },
+      {
+        "pagePath": "pages/profile/profile",
+        "iconPath": "images/profile.png",
+        "selectedIconPath": "images/profile-active.png",
+        "text": "我的"
+      }
+    ]
+  },
+  "style": "v2",
+  "componentFramework": "glass-easel",
+  "sitemapLocation": "sitemap.json",
+  "lazyCodeLoading": "requiredComponents"
+}

+ 10 - 0
app.wxss

@@ -0,0 +1,10 @@
+/**app.wxss**/
+.container {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: space-between;
+  padding: 200rpx 0;
+  box-sizing: border-box;
+} 

+ 44 - 0
images/README.md

@@ -0,0 +1,44 @@
+# 图标文件说明
+
+## 需要准备的图标文件
+
+请在 `images` 目录下添加以下图标文件(建议尺寸:81px × 81px,PNG格式):
+
+1. `home.png` - 首页未选中图标(灰色)
+2. `home-active.png` - 首页选中图标(主题色 #667eea)
+3. `bookshelf.png` - 书架未选中图标(灰色)
+4. `bookshelf-active.png` - 书架选中图标(主题色 #667eea)
+5. `profile.png` - 我的未选中图标(灰色)
+6. `profile-active.png` - 我的选中图标(主题色 #667eea)
+
+## 快速解决方案
+
+### 方案1:使用在线工具生成占位图标
+
+1. 访问 https://www.iconfont.cn/ 或 https://www.iconfinder.com/
+2. 搜索 "home"、"bookshelf"、"user" 等关键词
+3. 下载图标并重命名为对应的文件名
+4. 将图标放入 `images` 目录
+
+### 方案2:使用微信开发者工具生成
+
+1. 在微信开发者工具中,tabBar 图标是必需的
+2. 可以先用简单的占位图片(任何 81x81 的 PNG 图片)
+3. 后续再替换为正式图标
+
+### 方案3:暂时移除 tabBar(当前方案)
+
+目前 `app.json` 中已配置 tabBar,但暂时移除了图标路径。小程序可以正常运行,底部导航栏只显示文字。
+
+如果需要添加图标,请:
+1. 准备上述6个图标文件
+2. 将图标文件放入 `images` 目录
+3. 在 `app.json` 中恢复图标路径配置
+
+## 图标要求
+
+- 尺寸:81px × 81px(推荐)
+- 格式:PNG(支持透明背景)
+- 大小:单个文件不超过 40KB
+- 颜色:未选中使用灰色,选中使用主题色 #667eea
+<svg t="1762327730033" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4772" width="200" height="200"><path d="M947.701961 623.435294l-694.713725 0q-11.043137 0-23.090196 6.023529t-22.588235 18.070588-17.066667 28.611765-6.52549 37.647059 5.521569 39.152941 14.556863 32.12549 22.086275 22.086275 28.109804 8.031373l693.709804 0q-12.047059-12.047059-21.082353-27.105882-8.031373-13.05098-14.556863-31.121569t-6.52549-41.160784q0-26.101961 6.52549-43.168627t14.556863-28.109804q9.035294-12.047059 21.082353-21.082353zM947.701961 897.505882q0 17.066667-22.086275 17.066667l-733.866667 0q-34.133333 0-57.223529-14.054902t-37.647059-38.65098-20.580392-58.729412-6.023529-73.286275l0-582.27451q0-77.301961 41.160784-111.937255t117.458824-34.635294l34.133333 0 68.266667 0 93.364706 0q51.2 0 104.909804-0.501961t104.909804-0.501961l94.368627 0 70.27451 0 35.137255 0q28.109804 0 49.192157 11.545098t35.137255 32.12549 21.584314 48.690196 7.529412 60.235294l0 406.588235q-191.74902 0-345.34902-1.003922l-128.501961 0q-64.25098 0-114.94902-0.501961t-83.827451-0.501961l-37.145098 0q-21.082353 0-41.160784 12.54902t-36.141176 34.133333-26.101961 50.196078-10.039216 59.733333q0 38.14902 10.541176 68.768627t27.607843 51.701961 37.647059 32.627451 40.658824 11.545098l685.678431 0q8.031373 0 14.556863 4.517647t6.52549 14.556863zM515.011765 0l0 388.517647 77.301961-108.423529 76.298039 108.423529 0-388.517647-153.6 0z" p-id="4773"></path></svg>

BIN
images/bookshelf-active.png


BIN
images/bookshelf.png


BIN
images/home-active.png


BIN
images/home.png


BIN
images/profile-active.png


BIN
images/profile.png


+ 223 - 0
pages/audio/audio.js

@@ -0,0 +1,223 @@
+// pages/audio/audio.js
+const chapterApi = require('../../api/chapter');
+const historyApi = require('../../api/history');
+const userUtil = require('../../utils/user');
+
+Page({
+  data: {
+    contentId: null,
+    audioId: null,
+    audio: null,
+    chapters: [],
+    currentIndex: 0,
+    playing: false,
+    currentTime: 0,
+    duration: 0,
+    speed: 1.0, // 播放速度
+    loading: true,
+    audioContext: null
+  },
+
+  onLoad(options) {
+    const contentId = parseInt(options.contentId);
+    const audioId = parseInt(options.audioId);
+    
+    if (!contentId || !audioId) {
+      wx.showToast({
+        title: '参数错误',
+        icon: 'none'
+      });
+      setTimeout(() => {
+        wx.navigateBack();
+      }, 1500);
+      return;
+    }
+
+    this.setData({ contentId, audioId });
+    this.loadChapters();
+    this.loadAudio();
+    this.initAudio();
+  },
+
+  onUnload() {
+    // 页面卸载时停止播放
+    if (this.data.audioContext) {
+      this.data.audioContext.stop();
+    }
+  },
+
+  // 初始化音频上下文
+  initAudio() {
+    const audioContext = wx.createInnerAudioContext();
+    audioContext.onPlay(() => {
+      this.setData({ playing: true });
+    });
+    audioContext.onPause(() => {
+      this.setData({ playing: false });
+    });
+    audioContext.onTimeUpdate(() => {
+      this.setData({
+        currentTime: audioContext.currentTime,
+        duration: audioContext.duration
+      });
+    });
+    audioContext.onEnded(() => {
+      this.setData({ playing: false });
+      // 自动播放下一章
+      this.nextChapter();
+    });
+    audioContext.onError((err) => {
+      wx.showToast({
+        title: '播放出错',
+        icon: 'none'
+      });
+      console.error('音频播放错误:', err);
+    });
+    this.setData({ audioContext });
+  },
+
+  // 加载章节列表
+  async loadChapters() {
+    try {
+      const chapters = await chapterApi.getAudioChapterList(this.data.contentId);
+      const currentIndex = chapters.findIndex(c => c.audioId === this.data.audioId);
+      this.setData({ 
+        chapters,
+        currentIndex: currentIndex >= 0 ? currentIndex : 0
+      });
+    } catch (error) {
+      console.error('加载章节列表失败:', error);
+    }
+  },
+
+  // 加载音频信息
+  async loadAudio() {
+    this.setData({ loading: true });
+    try {
+      const audio = await chapterApi.getAudioChapter(this.data.audioId);
+      this.setData({ audio });
+      
+      // 设置音频源
+      if (this.data.audioContext && audio.audioUrl) {
+        this.data.audioContext.src = audio.audioUrl;
+        this.data.audioContext.playbackRate = this.data.speed;
+      }
+      
+      wx.setNavigationBarTitle({
+        title: audio.chapterTitle || '听书'
+      });
+    } catch (error) {
+      wx.showToast({
+        title: error || '加载失败',
+        icon: 'none'
+      });
+    } finally {
+      this.setData({ loading: false });
+    }
+  },
+
+  // 播放/暂停
+  togglePlay() {
+    if (!this.data.audioContext) return;
+    
+    if (this.data.playing) {
+      this.data.audioContext.pause();
+    } else {
+      this.data.audioContext.play();
+    }
+  },
+
+  // 上一章
+  prevChapter() {
+    if (this.data.currentIndex > 0) {
+      const prevChapter = this.data.chapters[this.data.currentIndex - 1];
+      this.setData({
+        audioId: prevChapter.audioId,
+        currentIndex: this.data.currentIndex - 1,
+        playing: false,
+        currentTime: 0
+      });
+      if (this.data.audioContext) {
+        this.data.audioContext.stop();
+      }
+      this.loadAudio();
+    } else {
+      wx.showToast({
+        title: '已经是第一章了',
+        icon: 'none'
+      });
+    }
+  },
+
+  // 下一章
+  nextChapter() {
+    if (this.data.currentIndex < this.data.chapters.length - 1) {
+      const nextChapter = this.data.chapters[this.data.currentIndex + 1];
+      this.setData({
+        audioId: nextChapter.audioId,
+        currentIndex: this.data.currentIndex + 1,
+        playing: false,
+        currentTime: 0
+      });
+      if (this.data.audioContext) {
+        this.data.audioContext.stop();
+      }
+      this.loadAudio();
+    } else {
+      wx.showToast({
+        title: '已经是最后一章了',
+        icon: 'none'
+      });
+    }
+  },
+
+  // 调整播放进度
+  onSeek(e) {
+    const time = e.detail.value;
+    if (this.data.audioContext) {
+      this.data.audioContext.seek(time);
+      this.setData({ currentTime: time });
+    }
+  },
+
+  // 调整播放速度
+  changeSpeed(e) {
+    const speed = parseFloat(e.detail.value);
+    this.setData({ speed });
+    if (this.data.audioContext) {
+      this.data.audioContext.playbackRate = speed;
+    }
+  },
+
+  // 显示章节列表
+  showChapterList() {
+    const itemList = this.data.chapters.map((item, index) => 
+      `${index + 1}. ${item.chapterTitle}`
+    );
+    
+    wx.showActionSheet({
+      itemList,
+      success: (res) => {
+        const chapter = this.data.chapters[res.tapIndex];
+        this.setData({
+          audioId: chapter.audioId,
+          currentIndex: res.tapIndex,
+          playing: false,
+          currentTime: 0
+        });
+        if (this.data.audioContext) {
+          this.data.audioContext.stop();
+        }
+        this.loadAudio();
+      }
+    });
+  },
+
+  // 格式化时间
+  formatTime(seconds) {
+    if (!seconds || isNaN(seconds)) return '00:00';
+    const mins = Math.floor(seconds / 60);
+    const secs = Math.floor(seconds % 60);
+    return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
+  }
+});

+ 4 - 0
pages/audio/audio.json

@@ -0,0 +1,4 @@
+{
+  "navigationBarTitleText": "听书",
+  "navigationBarTextStyle": "white"
+}

+ 66 - 0
pages/audio/audio.wxml

@@ -0,0 +1,66 @@
+<!--pages/audio/audio.wxml-->
+<view class="container" wx:if="{{!loading && audio}}">
+  <!-- 封面和标题 -->
+  <view class="header">
+    <image class="cover" src="/images/default-cover.png" mode="aspectFill"></image>
+    <view class="title">{{audio.chapterTitle}}</view>
+    <view class="narrator" wx:if="{{audio.narrator}}">播讲:{{audio.narrator}}</view>
+  </view>
+
+  <!-- 播放进度 -->
+  <view class="progress-section">
+    <text class="time-text">{{formatTime(currentTime)}}</text>
+    <slider 
+      class="progress-slider"
+      min="0"
+      max="{{duration || 100}}"
+      value="{{currentTime}}"
+      step="1"
+      bindchange="onSeek"
+      activeColor="#667eea"
+      backgroundColor="#e0e0e0"
+      block-color="#667eea"
+    />
+    <text class="time-text">{{formatTime(duration)}}</text>
+  </view>
+
+  <!-- 播放控制 -->
+  <view class="controls">
+    <view class="control-btn" bindtap="prevChapter">
+      <text class="icon">◀◀</text>
+      <text class="label">上一章</text>
+    </view>
+    <view class="control-btn play-btn" bindtap="togglePlay">
+      <text class="icon">{{playing ? '⏸' : '▶'}}</text>
+    </view>
+    <view class="control-btn" bindtap="nextChapter">
+      <text class="icon">▶▶</text>
+      <text class="label">下一章</text>
+    </view>
+  </view>
+
+  <!-- 播放设置 -->
+  <view class="settings">
+    <view class="setting-item">
+      <text class="setting-label">播放速度</text>
+      <slider 
+        class="speed-slider"
+        min="0.5"
+        max="2.0"
+        step="0.25"
+        value="{{speed}}"
+        bindchange="changeSpeed"
+        activeColor="#fff"
+        backgroundColor="rgba(255,255,255,0.3)"
+      />
+      <text class="speed-text">{{speed}}x</text>
+    </view>
+    <view class="setting-item">
+      <button class="chapter-list-btn" bindtap="showChapterList">查看目录</button>
+    </view>
+  </view>
+</view>
+
+<view class="loading" wx:if="{{loading}}">
+  <text>加载中...</text>
+</view>

+ 287 - 0
pages/audio/audio.wxss

@@ -0,0 +1,287 @@
+/* pages/audio/audio.wxss */
+.container {
+  min-height: 100vh;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  padding: 60rpx 30rpx;
+  color: #fff;
+  position: relative;
+  overflow: hidden;
+}
+
+.container::before {
+  content: '';
+  position: absolute;
+  top: -30%;
+  right: -20%;
+  width: 500rpx;
+  height: 500rpx;
+  background: rgba(255, 255, 255, 0.1);
+  border-radius: 50%;
+  pointer-events: none;
+}
+
+.container::after {
+  content: '';
+  position: absolute;
+  bottom: -20%;
+  left: -15%;
+  width: 400rpx;
+  height: 400rpx;
+  background: rgba(255, 255, 255, 0.08);
+  border-radius: 50%;
+  pointer-events: none;
+}
+
+.header {
+  text-align: center;
+  margin-bottom: 80rpx;
+  position: relative;
+  z-index: 1;
+}
+
+.cover {
+  width: 340rpx;
+  height: 340rpx;
+  border-radius: 24rpx;
+  margin: 0 auto 50rpx;
+  box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.4);
+  transition: transform 0.3s ease;
+  position: relative;
+  overflow: hidden;
+}
+
+.cover::after {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
+  opacity: 0;
+  transition: opacity 0.3s ease;
+  pointer-events: none;
+}
+
+.title {
+  font-size: 40rpx;
+  font-weight: 600;
+  margin-bottom: 24rpx;
+  text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
+  line-height: 1.4;
+}
+
+.narrator {
+  font-size: 28rpx;
+  opacity: 0.9;
+  font-weight: 500;
+}
+
+.progress-section {
+  display: flex;
+  align-items: center;
+  margin-bottom: 80rpx;
+  position: relative;
+  z-index: 1;
+  gap: 20rpx;
+}
+
+.time-text {
+  font-size: 26rpx;
+  min-width: 90rpx;
+  text-align: center;
+  font-weight: 500;
+  opacity: 0.95;
+}
+
+.progress-slider {
+  flex: 1;
+}
+
+.controls {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-bottom: 80rpx;
+  position: relative;
+  z-index: 1;
+  gap: 40rpx;
+}
+
+.control-btn {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  transition: all 0.3s ease;
+  padding: 10rpx;
+}
+
+.control-btn:active {
+  transform: scale(0.9);
+  opacity: 0.8;
+}
+
+.control-btn .icon {
+  font-size: 64rpx;
+  margin-bottom: 12rpx;
+  transition: transform 0.3s ease;
+}
+
+.control-btn:active .icon {
+  transform: scale(1.1);
+}
+
+.control-btn .label {
+  font-size: 24rpx;
+  opacity: 0.9;
+  font-weight: 500;
+}
+
+.play-btn {
+  width: 140rpx;
+  height: 140rpx;
+  border-radius: 50%;
+  background: rgba(255, 255, 255, 0.25);
+  backdrop-filter: blur(10rpx);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.3);
+  border: 4rpx solid rgba(255, 255, 255, 0.3);
+  transition: all 0.3s ease;
+  position: relative;
+}
+
+.play-btn::before {
+  content: '';
+  position: absolute;
+  top: -4rpx;
+  left: -4rpx;
+  right: -4rpx;
+  bottom: -4rpx;
+  border-radius: 50%;
+  background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.1) 100%);
+  opacity: 0;
+  transition: opacity 0.3s ease;
+  pointer-events: none;
+}
+
+.play-btn:active {
+  transform: scale(0.95);
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.4);
+}
+
+.play-btn:active::before {
+  opacity: 1;
+}
+
+.play-btn .icon {
+  font-size: 88rpx;
+  margin: 0;
+  text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
+}
+
+.settings {
+  background: rgba(255, 255, 255, 0.15);
+  backdrop-filter: blur(20rpx);
+  border-radius: 24rpx;
+  padding: 40rpx;
+  border: 2rpx solid rgba(255, 255, 255, 0.2);
+  box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
+  position: relative;
+  z-index: 1;
+}
+
+.setting-item {
+  margin-bottom: 40rpx;
+}
+
+.setting-item:last-child {
+  margin-bottom: 0;
+}
+
+.setting-label {
+  font-size: 30rpx;
+  margin-bottom: 24rpx;
+  display: block;
+  font-weight: 500;
+  opacity: 0.95;
+}
+
+.speed-slider {
+  margin-bottom: 12rpx;
+}
+
+.speed-text {
+  font-size: 26rpx;
+  opacity: 0.9;
+  text-align: right;
+  display: block;
+  font-weight: 600;
+  background: rgba(255, 255, 255, 0.2);
+  padding: 8rpx 16rpx;
+  border-radius: 12rpx;
+  display: inline-block;
+  float: right;
+}
+
+.chapter-list-btn {
+  width: 100%;
+  height: 88rpx;
+  line-height: 88rpx;
+  background: rgba(255, 255, 255, 0.25);
+  backdrop-filter: blur(10rpx);
+  color: #fff;
+  border-radius: 16rpx;
+  font-size: 30rpx;
+  font-weight: 500;
+  border: 2rpx solid rgba(255, 255, 255, 0.3);
+  transition: all 0.3s ease;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.2);
+}
+
+.chapter-list-btn::after {
+  border: none;
+}
+
+.chapter-list-btn:active {
+  background: rgba(255, 255, 255, 0.35);
+  transform: scale(0.98);
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.3);
+}
+
+.loading {
+  text-align: center;
+  padding: 200rpx 40rpx;
+  font-size: 28rpx;
+  color: #fff;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  position: relative;
+  z-index: 1;
+}
+
+.loading text {
+  margin-top: 20rpx;
+  opacity: 0.9;
+}
+
+.loading::before {
+  content: '';
+  width: 40rpx;
+  height: 40rpx;
+  border: 4rpx solid rgba(255, 255, 255, 0.3);
+  border-top-color: #fff;
+  border-radius: 50%;
+  animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}

+ 72 - 0
pages/bookshelf/bookshelf.js

@@ -0,0 +1,72 @@
+// pages/bookshelf/bookshelf.js
+const bookshelfApi = require('../../api/bookshelf');
+const contentApi = require('../../api/content');
+const userUtil = require('../../utils/user');
+
+Page({
+  data: {
+    bookshelf: [],
+    contentList: [],
+    loading: false
+  },
+
+  onLoad() {
+    this.loadBookshelf();
+  },
+
+  onShow() {
+    this.loadBookshelf();
+  },
+
+  // 加载书架
+  async loadBookshelf() {
+    if (!userUtil.isLogin()) {
+      this.setData({ bookshelf: [], contentList: [] });
+      return;
+    }
+
+    this.setData({ loading: true });
+    try {
+      const userInfo = userUtil.getUserInfo();
+      const bookshelf = await bookshelfApi.getBookshelfList(userInfo.userId);
+      
+      // 获取书籍详情
+      const contentList = [];
+      for (const item of bookshelf) {
+        try {
+          const content = await contentApi.getContentDetail(item.contentId);
+          contentList.push({
+            ...content,
+            addTime: item.addTime,
+            lastAccessTime: item.lastAccessTime
+          });
+        } catch (error) {
+          console.error('加载书籍详情失败:', error);
+        }
+      }
+      
+      this.setData({ 
+        bookshelf,
+        contentList
+      });
+      
+      // 同时保存到本地
+      wx.setStorageSync('bookshelf', contentList);
+    } catch (error) {
+      console.error('加载书架失败:', error);
+      // 失败时从本地加载
+      const localBookshelf = wx.getStorageSync('bookshelf') || [];
+      this.setData({ contentList: localBookshelf });
+    } finally {
+      this.setData({ loading: false });
+    }
+  },
+
+  // 跳转到详情
+  goToDetail(e) {
+    const contentId = e.currentTarget.dataset.id;
+    wx.navigateTo({
+      url: `/pages/detail/detail?id=${contentId}`
+    });
+  }
+});

+ 3 - 0
pages/bookshelf/bookshelf.json

@@ -0,0 +1,3 @@
+{
+  "navigationBarTitleText": "书架"
+}

+ 41 - 0
pages/bookshelf/bookshelf.wxml

@@ -0,0 +1,41 @@
+<!--pages/bookshelf/bookshelf.wxml-->
+<view class="container">
+  <view class="bookshelf-list" wx:if="{{contentList.length > 0}}">
+    <view 
+      class="bookshelf-item"
+      wx:for="{{contentList}}"
+      wx:key="contentId"
+      data-id="{{item.contentId}}"
+      bindtap="goToDetail"
+    >
+      <view class="cover-wrapper">
+        <image 
+          class="cover" 
+          src="{{item.coverUrl}}" 
+          mode="aspectFill" 
+          lazy-load="{{true}}"
+          wx:if="{{item.coverUrl}}"
+        ></image>
+        <view class="cover-placeholder" wx:else>
+          <text class="cover-text">{{item.title}}</text>
+        </view>
+      </view>
+      <view class="book-info">
+        <view class="title">{{item.title}}</view>
+        <view class="author">{{item.author || '未知作者'}}</view>
+        <view class="meta">
+          <text class="type">{{item.contentType === 1 ? '电子书' : '听书'}}</text>
+          <text class="chapters">{{item.totalChapters || 0}}章</text>
+        </view>
+      </view>
+    </view>
+  </view>
+  
+  <view class="empty" wx:if="{{!loading && contentList.length === 0}}">
+    <text>书架是空的,快去添加一些书籍吧~</text>
+  </view>
+  
+  <view class="loading" wx:if="{{loading}}">
+    <text>加载中...</text>
+  </view>
+</view>

+ 201 - 0
pages/bookshelf/bookshelf.wxss

@@ -0,0 +1,201 @@
+/* pages/bookshelf/bookshelf.wxss */
+.container {
+  min-height: 100vh;
+  background: linear-gradient(180deg, #f8f9ff 0%, #f5f5f5 100%);
+  padding: 30rpx 24rpx;
+}
+
+.bookshelf-list {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 24rpx;
+}
+
+.bookshelf-item {
+  background: #fff;
+  border-radius: 16rpx;
+  overflow: hidden;
+  box-shadow: 0 4rpx 20rpx rgba(102, 126, 234, 0.08);
+  transition: all 0.3s ease;
+  position: relative;
+}
+
+.bookshelf-item::before {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%);
+  opacity: 0;
+  transition: opacity 0.3s ease;
+  pointer-events: none;
+}
+
+.bookshelf-item:active {
+  transform: scale(0.98);
+  box-shadow: 0 2rpx 12rpx rgba(102, 126, 234, 0.12);
+}
+
+.bookshelf-item:active::before {
+  opacity: 1;
+}
+
+.cover-wrapper {
+  width: 100%;
+  height: 300rpx;
+  position: relative;
+  overflow: hidden;
+}
+
+.cover {
+  width: 100%;
+  height: 100%;
+  transition: transform 0.3s ease;
+}
+
+.bookshelf-item:active .cover {
+  transform: scale(1.05);
+}
+
+.cover-placeholder {
+  width: 100%;
+  height: 100%;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 12rpx;
+  box-sizing: border-box;
+  position: relative;
+}
+
+.cover-placeholder::after {
+  content: '';
+  position: absolute;
+  top: -50%;
+  left: -50%;
+  width: 200%;
+  height: 200%;
+  background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%);
+  animation: shine 3s infinite;
+}
+
+@keyframes shine {
+  0% {
+    transform: translateX(-100%) translateY(-100%) rotate(45deg);
+  }
+  100% {
+    transform: translateX(100%) translateY(100%) rotate(45deg);
+  }
+}
+
+.cover-text {
+  color: #fff;
+  font-size: 20rpx;
+  font-weight: 500;
+  text-align: center;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 3;
+  -webkit-box-orient: vertical;
+  line-height: 1.4;
+  position: relative;
+  z-index: 1;
+}
+
+.book-info {
+  padding: 24rpx 16rpx 20rpx;
+}
+
+.title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #1a1a1a;
+  margin-bottom: 12rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  line-height: 1.3;
+}
+
+.author {
+  font-size: 22rpx;
+  color: #8a8a8a;
+  margin-bottom: 12rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.meta {
+  display: flex;
+  gap: 8rpx;
+  flex-wrap: wrap;
+  align-items: center;
+}
+
+.meta text {
+  font-size: 20rpx;
+  color: #667eea;
+  padding: 4rpx 12rpx;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
+  border-radius: 12rpx;
+  font-weight: 500;
+}
+
+.empty {
+  text-align: center;
+  padding: 240rpx 40rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.empty::before {
+  content: '📚';
+  font-size: 120rpx;
+  margin-bottom: 30rpx;
+  opacity: 0.3;
+}
+
+.empty text {
+  font-size: 28rpx;
+  color: #999;
+  line-height: 1.6;
+}
+
+.loading {
+  text-align: center;
+  padding: 60rpx 40rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.loading text {
+  font-size: 26rpx;
+  color: #999;
+  margin-top: 20rpx;
+}
+
+.loading::before {
+  content: '';
+  width: 40rpx;
+  height: 40rpx;
+  border: 4rpx solid #f0f0f0;
+  border-top-color: #667eea;
+  border-radius: 50%;
+  animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}

+ 372 - 0
pages/chat/chat.js

@@ -0,0 +1,372 @@
+// pages/chat/chat.js
+const chatApi = require('../../api/chat');
+const userUtil = require('../../utils/user');
+
+Page({
+  data: {
+    userId: null,
+    sessionId: null,
+    messages: [],
+    inputMessage: '',
+    loading: false,
+    sending: false,
+    scrollIntoView: ''  // 用于scroll-view滚动到底部
+  },
+
+  onLoad(options) {
+    console.log('聊天页面加载, options:', options);
+    
+    const userInfo = userUtil.getUserInfo();
+    console.log('用户信息:', userInfo);
+    
+    if (!userInfo || !userInfo.userId) {
+      console.error('用户未登录或userId为空');
+      wx.showToast({
+        title: '请先登录',
+        icon: 'none'
+      });
+      setTimeout(() => {
+        wx.navigateBack();
+      }, 1500);
+      return;
+    }
+
+    console.log('设置userId:', userInfo.userId);
+    this.setData({
+      userId: userInfo.userId
+    });
+
+    // 优先使用传入的sessionId(如果有)
+    if (options.sessionId) {
+      console.log('使用传入的sessionId:', options.sessionId);
+      this.setData({
+        sessionId: options.sessionId
+      });
+      // 保存sessionId到本地存储
+      this.saveSessionId(options.sessionId);
+      this.loadMessageHistory();
+    } else {
+      // 尝试从本地存储加载sessionId
+      const savedSessionId = this.getSavedSessionId(userInfo.userId);
+      if (savedSessionId) {
+        console.log('从本地存储加载sessionId:', savedSessionId);
+        this.setData({
+          sessionId: savedSessionId
+        });
+        this.loadMessageHistory();
+      } else {
+        // 创建新会话
+        console.log('创建新会话');
+        this.createSession();
+      }
+    }
+  },
+  
+  onShow() {
+    // 检查用户是否仍然登录
+    const userInfo = userUtil.getUserInfo();
+    if (!userInfo || !userInfo.userId) {
+      // 用户已退出登录,清除会话数据
+      if (this.data.sessionId) {
+        this.clearSavedSessionId(this.data.userId);
+      }
+      return;
+    }
+    
+    // 如果userId发生变化(可能是切换了账号),重新加载
+    if (this.data.userId && userInfo.userId !== this.data.userId) {
+      console.log('检测到用户切换,重新加载会话');
+      this.setData({
+        userId: userInfo.userId,
+        sessionId: null,
+        messages: []
+      });
+      const savedSessionId = this.getSavedSessionId(userInfo.userId);
+      if (savedSessionId) {
+        this.setData({ sessionId: savedSessionId });
+        this.loadMessageHistory();
+      } else {
+        this.createSession();
+      }
+    }
+  },
+
+  // 创建新会话
+  async createSession() {
+    try {
+      console.log('开始创建会话, userId:', this.data.userId);
+      if (!this.data.userId) {
+        throw new Error('userId为空,请先登录');
+      }
+      
+      this.setData({ loading: true });
+      const session = await chatApi.createSession(this.data.userId);
+      console.log('创建会话成功:', session);
+      
+      if (!session || !session.sessionId) {
+        throw new Error('创建会话失败,未返回sessionId');
+      }
+      
+      // 保存sessionId到本地存储
+      this.saveSessionId(session.sessionId);
+      
+      this.setData({
+        sessionId: session.sessionId,
+        messages: [],
+        loading: false
+      });
+      console.log('会话创建完成, sessionId:', session.sessionId);
+    } catch (error) {
+      console.error('创建会话失败:', error);
+      wx.showToast({
+        title: '创建会话失败: ' + (error.message || error),
+        icon: 'none',
+        duration: 2000
+      });
+      this.setData({ loading: false });
+      throw error; // 重新抛出错误,让调用方知道失败
+    }
+  },
+  
+  // 保存sessionId到本地存储
+  saveSessionId(sessionId) {
+    if (!this.data.userId || !sessionId) {
+      return;
+    }
+    const key = `chat_sessionId_${this.data.userId}`;
+    wx.setStorageSync(key, sessionId);
+    console.log('保存sessionId到本地存储:', key, sessionId);
+  },
+  
+  // 从本地存储获取sessionId
+  getSavedSessionId(userId) {
+    if (!userId) {
+      return null;
+    }
+    const key = `chat_sessionId_${userId}`;
+    const sessionId = wx.getStorageSync(key);
+    console.log('从本地存储获取sessionId:', key, sessionId);
+    return sessionId || null;
+  },
+  
+  // 清除本地存储的sessionId
+  clearSavedSessionId(userId) {
+    if (!userId) {
+      return;
+    }
+    const key = `chat_sessionId_${userId}`;
+    wx.removeStorageSync(key);
+    console.log('清除本地存储的sessionId:', key);
+  },
+
+  // 加载消息历史
+  async loadMessageHistory() {
+    if (!this.data.sessionId || !this.data.userId) {
+      console.warn('无法加载消息历史:sessionId或userId为空');
+      return;
+    }
+    
+    try {
+      this.setData({ loading: true });
+      console.log('加载消息历史:', this.data.sessionId, this.data.userId);
+      const messages = await chatApi.getMessageHistory(this.data.sessionId, this.data.userId);
+      console.log('收到消息历史:', messages);
+      
+      // 确保消息格式正确(处理时间戳)
+      const formattedMessages = (messages || []).map(msg => {
+        return {
+          role: msg.role || 'user',
+          content: msg.content || '',
+          timestamp: msg.timestamp || new Date().toISOString()
+        };
+      });
+      
+      this.setData({
+        messages: formattedMessages,
+        loading: false
+      });
+      
+      // 延迟滚动到底部,确保DOM已更新
+      setTimeout(() => {
+        this.scrollToBottom();
+      }, 200);
+    } catch (error) {
+      console.error('加载消息历史失败:', error);
+      // 如果是会话不存在或无权访问,清除本地保存的sessionId
+      if (error.message && (error.message.includes('不存在') || error.message.includes('无权访问'))) {
+        console.log('会话无效,清除本地保存的sessionId');
+        this.clearSavedSessionId(this.data.userId);
+        this.setData({
+          sessionId: null,
+          messages: []
+        });
+        // 创建新会话
+        this.createSession();
+      } else {
+        wx.showToast({
+          title: '加载消息失败',
+          icon: 'none'
+        });
+        this.setData({ loading: false });
+      }
+    }
+  },
+
+  // 输入消息
+  onInput(e) {
+    this.setData({
+      inputMessage: e.detail.value
+    });
+  },
+
+  // 发送消息
+  async sendMessage() {
+    const message = this.data.inputMessage.trim();
+    if (!message) {
+      wx.showToast({
+        title: '请输入消息',
+        icon: 'none'
+      });
+      return;
+    }
+
+    // 检查userId
+    if (!this.data.userId) {
+      console.error('userId为空,请先登录');
+      wx.showToast({
+        title: '请先登录',
+        icon: 'none'
+      });
+      return;
+    }
+
+    // 如果没有sessionId,先创建会话
+    if (!this.data.sessionId) {
+      console.log('没有sessionId,创建新会话');
+      try {
+        await this.createSession();
+        if (!this.data.sessionId) {
+          wx.showToast({
+            title: '创建会话失败',
+            icon: 'none'
+          });
+          return;
+        }
+      } catch (error) {
+        console.error('创建会话失败:', error);
+        wx.showToast({
+          title: '创建会话失败: ' + error,
+          icon: 'none',
+          duration: 2000
+        });
+        return;
+      }
+    }
+
+    try {
+      this.setData({ 
+        sending: true,
+        inputMessage: ''
+      });
+
+      console.log('开始发送消息:', {
+        sessionId: this.data.sessionId,
+        userId: this.data.userId,
+        message: message
+      });
+
+      // 添加用户消息到列表
+      const userMessage = {
+        role: 'user',
+        content: message,
+        timestamp: new Date().toISOString()
+      };
+      const currentMessages = [...this.data.messages, userMessage];
+      this.setData({
+        messages: currentMessages
+      });
+      this.scrollToBottom();
+
+      // 发送消息到服务器
+      console.log('调用API发送消息...');
+      const response = await chatApi.sendMessage(this.data.sessionId, message, this.data.userId);
+      console.log('收到API响应:', response);
+      
+      // 更新会话ID(如果是新创建的)
+      if (response.session && response.session.sessionId) {
+        const newSessionId = response.session.sessionId;
+        console.log('更新sessionId:', newSessionId);
+        this.setData({
+          sessionId: newSessionId
+        });
+        // 保存新的sessionId到本地存储
+        this.saveSessionId(newSessionId);
+      }
+
+      // 添加AI回复到列表
+      if (response.assistantMessage) {
+        console.log('收到AI回复:', response.assistantMessage.content);
+        this.setData({
+          messages: [...currentMessages, response.assistantMessage]
+        });
+        this.scrollToBottom();
+      } else {
+        console.warn('未收到AI回复');
+        wx.showToast({
+          title: '未收到AI回复',
+          icon: 'none'
+        });
+      }
+
+      this.setData({ sending: false });
+    } catch (error) {
+      console.error('发送消息失败:', error);
+      console.error('错误详情:', JSON.stringify(error));
+      wx.showToast({
+        title: '发送失败: ' + (error.message || error || '未知错误'),
+        icon: 'none',
+        duration: 3000
+      });
+      // 移除用户消息(发送失败)
+      const messages = this.data.messages.filter(m => m.content !== message || m.role !== 'user');
+      this.setData({
+        messages: messages,
+        sending: false,
+        inputMessage: message
+      });
+    }
+  },
+
+  // 滚动到底部
+  scrollToBottom() {
+    // 使用scroll-view的scroll-into-view属性滚动到底部
+    if (this.data.messages && this.data.messages.length > 0) {
+      const lastIndex = this.data.messages.length - 1;
+      const scrollIntoView = `msg-${lastIndex}`;
+      this.setData({
+        scrollIntoView: scrollIntoView
+      });
+      // 清除scrollIntoView,以便下次可以再次触发
+      setTimeout(() => {
+        this.setData({
+          scrollIntoView: ''
+        });
+      }, 300);
+    }
+  },
+
+  // 格式化时间
+  formatTime(date) {
+    if (!date) return '';
+    try {
+      const d = typeof date === 'string' ? new Date(date) : date;
+      if (isNaN(d.getTime())) return '';
+      const hours = d.getHours().toString().padStart(2, '0');
+      const minutes = d.getMinutes().toString().padStart(2, '0');
+      return `${hours}:${minutes}`;
+    } catch (e) {
+      return '';
+    }
+  }
+});
+

+ 6 - 0
pages/chat/chat.json

@@ -0,0 +1,6 @@
+{
+  "navigationBarTitleText": "老舅AI智能客服",
+  "navigationBarBackgroundColor": "#667eea",
+  "navigationBarTextStyle": "white"
+}
+

+ 63 - 0
pages/chat/chat.wxml

@@ -0,0 +1,63 @@
+<!--pages/chat/chat.wxml-->
+<view class="container">
+  <!-- 消息列表 -->
+  <scroll-view 
+    class="messages-container" 
+    scroll-y="true" 
+    scroll-into-view="{{scrollIntoView}}"
+    id="messages"
+  >
+    <view class="messages" wx:if="{{messages.length > 0}}">
+      <view 
+        class="message-item {{item.role === 'user' ? 'user-message' : 'assistant-message'}}"
+        wx:for="{{messages}}"
+        wx:key="index"
+        id="msg-{{index}}"
+      >
+        <view class="message-row {{item.role === 'user' ? 'row-right' : 'row-left'}}">
+          <view class="avatar {{item.role === 'user' ? 'avatar-user' : 'avatar-assistant'}}">
+            <text>{{item.role === 'user' ? '我' : '老'}}</text>
+          </view>
+          <view class="message-content">
+            <text class="message-text">{{item.content}}</text>
+          </view>
+        </view>
+        <view class="message-time {{item.role === 'user' ? 'time-right' : 'time-left'}}">{{formatTime(item.timestamp)}}</view>
+      </view>
+    </view>
+    
+    <!-- 加载中 -->
+    <view class="loading" wx:if="{{loading}}">
+      <text>加载中...</text>
+    </view>
+    
+    <!-- 空状态 -->
+    <view class="empty" wx:if="{{!loading && messages.length === 0}}">
+      <view class="empty-icon">💬</view>
+      <text class="empty-title">老舅AI智能客服</text>
+      <text class="empty-tip">开始对话吧~</text>
+    </view>
+  </scroll-view>
+
+  <!-- 输入区域 -->
+  <view class="input-container">
+    <input 
+      class="input-box" 
+      type="text" 
+      placeholder="输入消息..." 
+      value="{{inputMessage}}"
+      bindinput="onInput"
+      bindconfirm="sendMessage"
+      disabled="{{sending}}"
+    />
+    <button 
+      class="send-btn" 
+      bindtap="sendMessage"
+      disabled="{{sending || !inputMessage.trim()}}"
+    >
+      {{sending ? '发送中...' : '发送'}}
+    </button>
+  </view>
+</view>
+
+ 

+ 183 - 0
pages/chat/chat.wxss

@@ -0,0 +1,183 @@
+/* pages/chat/chat.wxss */
+.container {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  background: linear-gradient(180deg, #f8f9ff 0%, #f5f5f5 100%);
+}
+
+.messages-container {
+  flex: 1;
+  padding: 30rpx;
+  overflow-y: auto;
+}
+
+.messages {
+  display: flex;
+  flex-direction: column;
+  gap: 30rpx;
+}
+
+.message-row {
+  display: flex;
+  align-items: flex-end;
+  gap: 16rpx;
+}
+
+.row-right {
+  flex-direction: row-reverse;
+}
+
+.row-left {
+  flex-direction: row;
+}
+
+.message-item {
+  display: flex;
+  flex-direction: column;
+  max-width: 70%;
+  animation: fadeIn 0.3s ease;
+}
+
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+    transform: translateY(10rpx);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.user-message {
+  align-self: flex-end;
+}
+
+.user-message .message-content {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  border-radius: 20rpx 20rpx 4rpx 20rpx;
+  padding: 20rpx 24rpx;
+  box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
+}
+
+.assistant-message {
+  align-self: flex-start;
+}
+
+.assistant-message .message-content {
+  background: #fff;
+  color: #333;
+  border-radius: 20rpx 20rpx 20rpx 4rpx;
+  padding: 20rpx 24rpx;
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
+  border: 1rpx solid #f0f0f0;
+}
+
+.avatar {
+  width: 56rpx;
+  height: 56rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #fff;
+  box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.08);
+}
+
+.avatar-user {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.avatar-assistant {
+  background: linear-gradient(135deg, #36d1dc 0%, #5b86e5 100%);
+}
+
+.message-text {
+  font-size: 28rpx;
+  line-height: 1.6;
+  word-wrap: break-word;
+}
+
+.message-time {
+  font-size: 22rpx;
+  color: #999;
+  margin-top: 8rpx;
+  padding: 0 8rpx;
+}
+
+.user-message .message-time, .time-right {
+  text-align: right;
+}
+
+.assistant-message .message-time, .time-left {
+  text-align: left;
+}
+
+.input-container {
+  display: flex;
+  align-items: center;
+  padding: 20rpx 30rpx;
+  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+  background: #fff;
+  border-top: 1rpx solid #e0e0e0;
+  box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
+}
+
+.input-box {
+  flex: 1;
+  height: 72rpx;
+  padding: 0 24rpx;
+  background: #f5f5f5;
+  border-radius: 36rpx;
+  font-size: 28rpx;
+  margin-right: 20rpx;
+  transition: background 0.2s ease, box-shadow 0.2s ease;
+}
+
+.input-box:focus {
+  background: #fff;
+  box-shadow: 0 0 0 4rpx rgba(102,126,234,0.15);
+}
+
+.send-btn {
+  width: 120rpx;
+  height: 72rpx;
+  line-height: 72rpx;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  border-radius: 36rpx;
+  font-size: 28rpx;
+  border: none;
+  font-weight: 500;
+}
+
+.send-btn::after {
+  border: none;
+}
+
+.send-btn[disabled] {
+  background: #ccc;
+  color: #999;
+}
+
+.loading {
+  text-align: center;
+  padding: 40rpx;
+  font-size: 26rpx;
+  color: #999;
+}
+
+.empty {
+  text-align: center;
+  padding: 160rpx 40rpx;
+  color: #8b8b8b;
+}
+
+.empty-icon { font-size: 120rpx; opacity: 0.25; }
+.empty-title { display:block; margin-top: 24rpx; font-size: 30rpx; color:#666; font-weight: 600; }
+.empty-tip { display:block; margin-top: 10rpx; font-size: 26rpx; color:#9aa0a6; }
+

+ 225 - 0
pages/detail/detail.js

@@ -0,0 +1,225 @@
+// pages/detail/detail.js
+const contentApi = require('../../api/content');
+const chapterApi = require('../../api/chapter');
+const bookshelfApi = require('../../api/bookshelf');
+const userUtil = require('../../utils/user');
+
+Page({
+  data: {
+    contentId: null,
+    content: null,
+    chapters: [],
+    loading: true,
+    contentType: null,
+    inBookshelf: false,
+    showChapterList: false
+  },
+
+  onLoad(options) {
+    const contentId = parseInt(options.id);
+    if (!contentId) {
+      wx.showToast({
+        title: '参数错误',
+        icon: 'none'
+      });
+      setTimeout(() => {
+        wx.navigateBack();
+      }, 1500);
+      return;
+    }
+
+    this.setData({ contentId });
+    this.loadContentDetail();
+  },
+
+  // 加载内容详情
+  async loadContentDetail() {
+    try {
+      const content = await contentApi.getContentDetail(this.data.contentId);
+      this.setData({
+        content,
+        contentType: content.contentType
+      });
+      wx.setNavigationBarTitle({
+        title: content.title
+      });
+
+      // 增加浏览量
+      try {
+        const viewData = await contentApi.increaseViewCount(this.data.contentId);
+        if (viewData && typeof viewData.viewCount === 'number') {
+          this.setData({
+            'content.viewCount': viewData.viewCount
+          });
+        }
+      } catch (error) {
+        console.error('更新浏览量失败:', error);
+      }
+      
+      // 加载详情后,再加载章节和检查书架状态
+      this.loadChapters();
+      this.checkBookshelfStatus();
+    } catch (error) {
+      wx.showToast({
+        title: error || '加载失败',
+        icon: 'none'
+      });
+    } finally {
+      this.setData({ loading: false });
+    }
+  },
+
+  // 加载章节列表
+  async loadChapters() {
+    try {
+      let chapters = [];
+      if (this.data.contentType === 1) {
+        // 电子书章节
+        chapters = await chapterApi.getBookChapterList(this.data.contentId);
+      } else if (this.data.contentType === 2) {
+        // 听书章节
+        chapters = await chapterApi.getAudioChapterList(this.data.contentId);
+      }
+      this.setData({ chapters });
+    } catch (error) {
+      console.error('加载章节失败:', error);
+      this.setData({ chapters: [] });
+    }
+  },
+
+  // 检查书架状态
+  async checkBookshelfStatus() {
+    if (!userUtil.isLogin()) return;
+    
+    try {
+      const userInfo = userUtil.getUserInfo();
+      const isIn = await bookshelfApi.checkInBookshelf(userInfo.userId, this.data.contentId);
+      this.setData({ inBookshelf: isIn });
+    } catch (error) {
+      console.error('检查书架状态失败:', error);
+    }
+  },
+
+  // 加入/移出书架
+  async toggleBookshelf() {
+    if (!userUtil.isLogin()) {
+      wx.showToast({
+        title: '请先登录',
+        icon: 'none'
+      });
+      setTimeout(() => {
+        wx.reLaunch({
+          url: '/pages/login/login'
+        });
+      }, 1500);
+      return;
+    }
+
+    try {
+      const userInfo = userUtil.getUserInfo();
+      const { contentId, inBookshelf } = this.data;
+      
+      if (inBookshelf) {
+        // 移出书架
+        await bookshelfApi.removeFromBookshelf(userInfo.userId, contentId);
+        this.setData({ inBookshelf: false });
+        wx.showToast({
+          title: '已移出书架',
+          icon: 'success'
+        });
+      } else {
+        // 加入书架
+        await bookshelfApi.addToBookshelf(userInfo.userId, contentId);
+        this.setData({ inBookshelf: true });
+        wx.showToast({
+          title: '已加入书架',
+          icon: 'success'
+        });
+      }
+    } catch (error) {
+      wx.showToast({
+        title: error || '操作失败',
+        icon: 'none'
+      });
+    }
+  },
+
+  // 开始阅读/听书
+  startRead() {
+    if (!userUtil.isLogin()) {
+      wx.showToast({
+        title: '请先登录',
+        icon: 'none'
+      });
+      setTimeout(() => {
+        wx.reLaunch({
+          url: '/pages/login/login'
+        });
+      }, 1500);
+      return;
+    }
+
+    const { contentType, contentId } = this.data;
+    
+    if (contentType === 1) {
+      // 电子书
+      if (this.data.chapters.length === 0) {
+        wx.showToast({
+          title: '暂无章节',
+          icon: 'none'
+        });
+        return;
+      }
+      // 跳转到阅读页,默认第一章
+      const firstChapter = this.data.chapters[0];
+      wx.navigateTo({
+        url: `/pages/read/read?contentId=${contentId}&chapterId=${firstChapter.chapterId}`
+      });
+    } else if (contentType === 2) {
+      // 听书
+      if (this.data.chapters.length === 0) {
+        wx.showToast({
+          title: '暂无章节',
+          icon: 'none'
+        });
+        return;
+      }
+      // 跳转到听书页,默认第一章
+      const firstChapter = this.data.chapters[0];
+      wx.navigateTo({
+        url: `/pages/audio/audio?contentId=${contentId}&audioId=${firstChapter.audioId}`
+      });
+    }
+  },
+
+  // 显示/隐藏章节列表
+  toggleChapterList() {
+    this.setData({
+      showChapterList: !this.data.showChapterList
+    });
+  },
+
+  // 选择章节
+  selectChapter(e) {
+    const index = e.currentTarget.dataset.index;
+    const chapter = this.data.chapters[index];
+    const { contentType, contentId } = this.data;
+    
+    this.setData({ showChapterList: false });
+    
+    if (contentType === 1) {
+      wx.navigateTo({
+        url: `/pages/read/read?contentId=${contentId}&chapterId=${chapter.chapterId}`
+      });
+    } else if (contentType === 2) {
+      wx.navigateTo({
+        url: `/pages/audio/audio?contentId=${contentId}&audioId=${chapter.audioId}`
+      });
+    }
+  },
+
+  // 封面加载错误
+  onCoverError(e) {
+    console.error('封面加载失败:', e);
+  }
+});

+ 3 - 0
pages/detail/detail.json

@@ -0,0 +1,3 @@
+{
+  "navigationBarTitleText": "详情"
+}

+ 98 - 0
pages/detail/detail.wxml

@@ -0,0 +1,98 @@
+<!--pages/detail/detail.wxml-->
+<view class="container" wx:if="{{!loading && content}}">
+  <!-- 封面和基本信息 -->
+  <view class="header">
+    <view class="cover-wrapper">
+      <image 
+        class="cover" 
+        src="{{content.coverUrl}}" 
+        mode="aspectFill" 
+        lazy-load="{{true}}"
+        binderror="onCoverError"
+        wx:if="{{content.coverUrl}}"
+      ></image>
+      <view class="cover-placeholder" wx:else>
+        <text class="cover-text">{{content.title}}</text>
+      </view>
+    </view>
+    <view class="info">
+      <view class="title">{{content.title}}</view>
+      <view class="author">作者:{{content.author || '未知'}}</view>
+      <view class="meta">
+        <text class="type">{{content.contentType === 1 ? '电子书' : '听书'}}</text>
+        <text class="chapters">{{content.totalChapters || 0}}章</text>
+        <text class="status">{{content.status === 1 ? '连载中' : '已完结'}}</text>
+      </view>
+      <view class="publisher" wx:if="{{content.publisher}}">
+        出版社:{{content.publisher}}
+      </view>
+    </view>
+  </view>
+
+  <!-- 简介 -->
+  <view class="description">
+    <view class="section-title">简介</view>
+    <view class="desc-text">{{content.description || '暂无简介'}}</view>
+  </view>
+
+  <!-- 章节列表 -->
+  <view class="chapter-section" wx:if="{{chapters.length > 0}}">
+    <view class="section-title">
+      <text>目录</text>
+      <text class="chapter-count">共{{chapters.length}}章</text>
+    </view>
+    <view class="chapter-list" wx:if="{{showChapterList}}">
+      <view 
+        class="chapter-item"
+        wx:for="{{chapters}}"
+        wx:key="chapterId"
+        wx:for-item="chapter"
+        wx:for-index="index"
+        data-index="{{index}}"
+        bindtap="selectChapter"
+      >
+        <text class="chapter-number">{{index + 1}}</text>
+        <text class="chapter-title">{{chapter.chapterTitle || chapter.title}}</text>
+        <text class="chapter-free" wx:if="{{chapter.isFree === 1}}">免费</text>
+      </view>
+    </view>
+    <view class="chapter-preview" wx:else>
+      <view 
+        class="chapter-item"
+        wx:for="{{chapters}}"
+        wx:key="chapterId"
+        wx:for-item="chapter"
+        wx:for-index="index"
+        data-index="{{index}}"
+        bindtap="selectChapter"
+      >
+        <text class="chapter-number">{{index + 1}}</text>
+        <text class="chapter-title">{{chapter.chapterTitle || chapter.title}}</text>
+        <text class="chapter-free" wx:if="{{chapter.isFree === 1}}">免费</text>
+      </view>
+    </view>
+  </view>
+  
+  <!-- 无章节提示 -->
+  <view class="no-chapters" wx:if="{{!loading && chapters.length === 0}}">
+    <text>暂无章节</text>
+  </view>
+
+  <!-- 操作按钮 -->
+  <view class="actions">
+    <button class="action-btn" bindtap="toggleBookshelf">
+      <text class="btn-icon">{{inBookshelf ? '✓' : '+'}}</text>
+      <text>{{inBookshelf ? '已在书架' : '加入书架'}}</text>
+    </button>
+    <button class="action-btn primary" bindtap="startRead">
+      {{contentType === 1 ? '开始阅读' : '开始听书'}}
+    </button>
+    <button class="action-btn" bindtap="toggleChapterList" wx:if="{{chapters.length > 0}}">
+      {{showChapterList ? '收起' : '目录'}}
+    </button>
+  </view>
+</view>
+
+<view class="loading" wx:if="{{loading}}">
+  <text>加载中...</text>
+</view>

+ 391 - 0
pages/detail/detail.wxss

@@ -0,0 +1,391 @@
+/* pages/detail/detail.wxss */
+page {
+  padding: 0;
+  margin: 0;
+}
+
+.container {
+  min-height: 100vh;
+  background: linear-gradient(180deg, #f8f9ff 0%, #f5f5f5 100%);
+  padding-bottom: 200rpx;
+  padding-top: 0;
+  margin-top: 0;
+}
+
+.header {
+  display: flex;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(118, 75, 162, 0.05) 100%);
+  padding: 30rpx 20rpx;
+  margin-bottom: 24rpx;
+  border-bottom: 1rpx solid rgba(102, 126, 234, 0.1);
+  padding-top: calc(30rpx + env(safe-area-inset-top));
+}
+
+.cover-wrapper {
+  width: 220rpx;
+  height: 300rpx;
+  margin-right: 32rpx;
+  flex-shrink: 0;
+  border-radius: 16rpx;
+  overflow: hidden;
+  box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.2);
+  position: relative;
+}
+
+.cover-wrapper::after {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
+  opacity: 0;
+  transition: opacity 0.3s ease;
+  pointer-events: none;
+}
+
+.cover {
+  width: 100%;
+  height: 100%;
+  transition: transform 0.3s ease;
+}
+
+.cover-placeholder {
+  width: 100%;
+  height: 100%;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 16rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 24rpx;
+  box-sizing: border-box;
+  position: relative;
+}
+
+.cover-text {
+  color: #fff;
+  font-size: 26rpx;
+  font-weight: 500;
+  text-align: center;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 4;
+  -webkit-box-orient: vertical;
+  line-height: 1.5;
+}
+
+.info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  min-width: 0;
+}
+
+.title {
+  font-size: 38rpx;
+  font-weight: 600;
+  color: #1a1a1a;
+  margin-bottom: 20rpx;
+  line-height: 1.4;
+}
+
+.author {
+  font-size: 28rpx;
+  color: #666;
+  margin-bottom: 20rpx;
+  font-weight: 500;
+}
+
+.meta {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12rpx;
+  margin-bottom: 20rpx;
+}
+
+.meta text {
+  font-size: 24rpx;
+  color: #667eea;
+  padding: 8rpx 18rpx;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
+  border-radius: 16rpx;
+  font-weight: 500;
+}
+
+.publisher {
+  font-size: 26rpx;
+  color: #8a8a8a;
+  font-weight: 500;
+}
+
+.description {
+  background: #fff;
+  padding: 40rpx 20rpx;
+  margin: 0 0 24rpx 0;
+  border-radius: 0;
+  box-shadow: 0 4rpx 20rpx rgba(102, 126, 234, 0.08);
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.section-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #1a1a1a;
+  margin-bottom: 24rpx;
+  position: relative;
+  padding-left: 20rpx;
+}
+
+.section-title::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 6rpx;
+  height: 28rpx;
+  background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
+  border-radius: 3rpx;
+}
+
+.desc-text {
+  font-size: 28rpx;
+  color: #666;
+  line-height: 1.9;
+  text-align: justify;
+}
+
+.actions {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  display: flex;
+  background: #fff;
+  padding: 24rpx 30rpx;
+  padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
+  box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
+  gap: 16rpx;
+}
+
+.action-btn {
+  flex: 1;
+  min-height: 88rpx;
+  text-align: center;
+  border-radius: 16rpx;
+  font-size: 28rpx;
+  font-weight: 500;
+  border: none;
+  transition: all 0.3s ease;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 4rpx;
+  padding: 12rpx 8rpx;
+  box-sizing: border-box;
+}
+
+.action-btn::after {
+  border: none;
+}
+
+.action-btn.primary {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.3);
+}
+
+.action-btn.primary:active {
+  transform: scale(0.98);
+  box-shadow: 0 2rpx 12rpx rgba(102, 126, 234, 0.4);
+}
+
+.action-btn:not(.primary) {
+  background: #fff;
+  color: #1a1a1a;
+  border: 2rpx solid #c0c0c0;
+  font-weight: 600;
+}
+
+.action-btn:not(.primary):active {
+  background: #f5f5f5;
+  border-color: #999;
+  transform: scale(0.98);
+}
+
+.action-btn.primary {
+  flex-direction: row;
+  gap: 8rpx;
+}
+
+.btn-icon {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #667eea;
+  line-height: 1;
+  display: block;
+}
+
+.action-btn.primary .btn-icon {
+  display: none;
+}
+
+.chapter-section {
+  background: #fff;
+  padding: 40rpx 20rpx;
+  margin: 0 0 24rpx 0;
+  border-radius: 0;
+  box-shadow: 0 4rpx 20rpx rgba(102, 126, 234, 0.08);
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.chapter-section .section-title {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #1a1a1a;
+  margin-bottom: 24rpx;
+  padding-left: 20rpx;
+}
+
+.chapter-count {
+  font-size: 26rpx;
+  font-weight: normal;
+  color: #8a8a8a;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
+  padding: 4rpx 14rpx;
+  border-radius: 12rpx;
+}
+
+.chapter-list,
+.chapter-preview {
+  max-height: 600rpx;
+  overflow-y: auto;
+}
+
+.chapter-item {
+  display: flex;
+  align-items: center;
+  padding: 24rpx 0;
+  border-bottom: 1rpx solid #f0f0f0;
+  transition: all 0.3s ease;
+  position: relative;
+}
+
+.chapter-item::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 0;
+  bottom: 0;
+  width: 4rpx;
+  background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
+  opacity: 0;
+  transition: opacity 0.3s ease;
+}
+
+.chapter-item:active {
+  background: linear-gradient(90deg, rgba(102, 126, 234, 0.05) 0%, transparent 100%);
+  padding-left: 12rpx;
+}
+
+.chapter-item:active::before {
+  opacity: 1;
+}
+
+.chapter-item:last-child {
+  border-bottom: none;
+}
+
+.chapter-number {
+  width: 60rpx;
+  font-size: 24rpx;
+  color: #999;
+  text-align: center;
+  font-weight: 500;
+  flex-shrink: 0;
+}
+
+.chapter-title {
+  flex: 1;
+  font-size: 28rpx;
+  color: #333;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  margin: 0 16rpx;
+  font-weight: 500;
+}
+
+.chapter-free {
+  font-size: 22rpx;
+  color: #667eea;
+  padding: 6rpx 14rpx;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%);
+  border-radius: 12rpx;
+  font-weight: 500;
+  flex-shrink: 0;
+}
+
+.no-chapters {
+  background: #fff;
+  padding: 80rpx 20rpx;
+  text-align: center;
+  color: #999;
+  font-size: 28rpx;
+  margin: 0 0 24rpx 0;
+  border-radius: 0;
+  box-shadow: 0 4rpx 20rpx rgba(102, 126, 234, 0.08);
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.no-chapters::before {
+  content: '📑';
+  font-size: 80rpx;
+  display: block;
+  margin-bottom: 20rpx;
+  opacity: 0.3;
+}
+
+.loading {
+  text-align: center;
+  padding: 200rpx 40rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.loading text {
+  font-size: 28rpx;
+  color: #999;
+  margin-top: 20rpx;
+}
+
+.loading::before {
+  content: '';
+  width: 40rpx;
+  height: 40rpx;
+  border: 4rpx solid #f0f0f0;
+  border-top-color: #667eea;
+  border-radius: 50%;
+  animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}

+ 113 - 0
pages/history/history.js

@@ -0,0 +1,113 @@
+// pages/history/history.js
+const historyApi = require('../../api/history');
+const userUtil = require('../../utils/user');
+
+Page({
+  data: {
+    history: [],
+    loading: false
+  },
+
+  onLoad() {
+    this.loadHistory();
+  },
+
+  onShow() {
+    this.loadHistory();
+  },
+
+  // 加载历史记录
+  async loadHistory() {
+    if (!userUtil.isLogin()) {
+      this.setData({ history: [] });
+      return;
+    }
+    
+    this.setData({ loading: true });
+    try {
+      const historyList = await historyApi.getHistoryList();
+      // 格式化时间显示
+      const formattedHistory = (historyList || []).map(item => {
+        const readTime = item.readTime || '';
+        let timeStr = '';
+        if (readTime) {
+          try {
+            const date = new Date(readTime);
+            const now = new Date();
+            const diff = now - date;
+            const days = Math.floor(diff / (1000 * 60 * 60 * 24));
+            const hours = Math.floor(diff / (1000 * 60 * 60));
+            const minutes = Math.floor(diff / (1000 * 60));
+            
+            if (days > 0) {
+              timeStr = `${days}天前`;
+            } else if (hours > 0) {
+              timeStr = `${hours}小时前`;
+            } else if (minutes > 0) {
+              timeStr = `${minutes}分钟前`;
+            } else {
+              timeStr = '刚刚';
+            }
+          } catch (e) {
+            timeStr = readTime.substring(0, 10);
+          }
+        }
+        return {
+          ...item,
+          readTime: timeStr
+        };
+      });
+      this.setData({ history: formattedHistory });
+    } catch (error) {
+      console.error('加载历史记录失败:', error);
+      wx.showToast({
+        title: error || '加载失败',
+        icon: 'none'
+      });
+      this.setData({ history: [] });
+    } finally {
+      this.setData({ loading: false });
+    }
+  },
+
+  // 跳转到详情
+  goToDetail(e) {
+    const contentId = e.currentTarget.dataset.id;
+    wx.navigateTo({
+      url: `/pages/detail/detail?id=${contentId}`
+    });
+  },
+
+  // 清空历史
+  async clearHistory() {
+    if (!userUtil.isLogin()) {
+      wx.showToast({
+        title: '请先登录',
+        icon: 'none'
+      });
+      return;
+    }
+    
+    wx.showModal({
+      title: '提示',
+      content: '确定要清空历史记录吗?',
+      success: async (res) => {
+        if (res.confirm) {
+          try {
+            await historyApi.clearHistory();
+            this.setData({ history: [] });
+            wx.showToast({
+              title: '已清空',
+              icon: 'success'
+            });
+          } catch (error) {
+            wx.showToast({
+              title: error || '清空失败',
+              icon: 'none'
+            });
+          }
+        }
+      }
+    });
+  }
+});

+ 3 - 0
pages/history/history.json

@@ -0,0 +1,3 @@
+{
+  "navigationBarTitleText": "阅读历史"
+}

+ 31 - 0
pages/history/history.wxml

@@ -0,0 +1,31 @@
+<!--pages/history/history.wxml-->
+<view class="container">
+  <view class="loading" wx:if="{{loading}}">
+    <text>加载中...</text>
+  </view>
+  
+  <view class="history-grid" wx:elif="{{history.length > 0}}">
+    <view 
+      class="history-item"
+      wx:for="{{history}}"
+      wx:key="contentId"
+      data-id="{{item.contentId}}"
+      bindtap="goToDetail"
+    >
+      <image class="cover" src="{{item.coverUrl}}" mode="aspectFill" lazy-load="{{true}}"></image>
+      <view class="book-info">
+        <view class="book-title">{{item.title}}</view>
+        <view class="author">{{item.author || '未知作者'}}</view>
+        <view class="time">{{item.readTime}}</view>
+      </view>
+    </view>
+  </view>
+  
+  <view class="empty" wx:else>
+    <text>暂无阅读历史</text>
+  </view>
+  
+  <view class="footer" wx:if="{{history.length > 0 && !loading}}">
+    <button class="clear-btn" bindtap="clearHistory">清空历史</button>
+  </view>
+</view>

+ 132 - 0
pages/history/history.wxss

@@ -0,0 +1,132 @@
+/* pages/history/history.wxss */
+page {
+  padding: 0;
+  margin: 0;
+}
+
+.container {
+  min-height: 100vh;
+  background: #f5f5f5;
+  display: flex;
+  flex-direction: column;
+  padding-bottom: 120rpx;
+  padding-top: 0;
+  margin-top: 0;
+}
+
+.history-grid {
+  display: flex;
+  flex-wrap: wrap;
+  padding: 0 15rpx 20rpx 15rpx;
+  flex: 1;
+  margin-top: 0;
+}
+
+.history-item {
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+  border-radius: 12rpx;
+  overflow: hidden;
+  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
+  transition: transform 0.2s ease;
+  width: 345rpx;
+  margin: 0 5rpx 20rpx 5rpx;
+}
+
+.history-item:active {
+  transform: scale(0.98);
+}
+
+.cover {
+  width: 100%;
+  height: 400rpx;
+  border-radius: 0;
+}
+
+.book-info {
+  display: flex;
+  flex-direction: column;
+  padding: 20rpx;
+  min-height: 140rpx;
+}
+
+.book-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 8rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  line-height: 1.4;
+  min-height: 78rpx;
+}
+
+.author {
+  font-size: 24rpx;
+  color: #999;
+  margin-bottom: 8rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.time {
+  font-size: 22rpx;
+  color: #bbb;
+  margin-top: auto;
+}
+
+.empty {
+  text-align: center;
+  padding: 200rpx 40rpx;
+  font-size: 28rpx;
+  color: #999;
+  flex: 1;
+}
+
+.loading {
+  text-align: center;
+  padding: 200rpx 40rpx;
+  font-size: 28rpx;
+  color: #999;
+  flex: 1;
+}
+
+.footer {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background: #fff;
+  padding: 20rpx 30rpx;
+  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+  border-top: 1rpx solid #e0e0e0;
+  box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
+}
+
+.clear-btn {
+  width: 100%;
+  height: 88rpx;
+  line-height: 88rpx;
+  text-align: center;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  border-radius: 16rpx;
+  font-size: 30rpx;
+  font-weight: 500;
+  border: none;
+  box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.3);
+}
+
+.clear-btn::after {
+  border: none;
+}
+
+.clear-btn:active {
+  transform: scale(0.98);
+  box-shadow: 0 2rpx 12rpx rgba(102, 126, 234, 0.4);
+}

+ 200 - 0
pages/index/index.js

@@ -0,0 +1,200 @@
+// pages/index/index.js
+const contentApi = require('../../api/content');
+const categoryApi = require('../../api/category');
+const userUtil = require('../../utils/user');
+
+Page({
+  data: {
+    categories: [],
+    currentCategory: 0, // 0表示全部
+    contentList: [],
+    bannerList: [], // 轮播图数据
+    recommendedList: [], // 今日推荐数据
+    contentType: null, // null全部, 1电子书, 2听书
+    keyword: '',
+    current: 1,
+    size: 10,
+    total: 0,
+    loading: false,
+    hasMore: true,
+    showSearch: false,
+    swiperMarginTop: 'calc(130rpx + env(safe-area-inset-top))', // 轮播图的上边距
+    recommendedColumns: 3 // 推荐区域每行显示数量(2或3)
+  },
+
+  onLoad() {
+    this.loadCategories();
+    this.loadBannerList();
+    this.loadRecommended();
+    this.loadContentList();
+  },
+
+  onShow() {
+    // 首页不需要强制登录,可以浏览
+  },
+
+  // 加载分类
+  async loadCategories() {
+    try {
+      const categories = await categoryApi.getCategoryList();
+      this.setData({
+        categories: [{ categoryId: 0, categoryName: '全部' }, ...categories]
+      });
+    } catch (error) {
+      console.error('加载分类失败:', error);
+    }
+  },
+
+  // 加载轮播图数据(取前5条热门内容)
+  async loadBannerList() {
+    try {
+      const result = await contentApi.getContentList(1, 5, '', null, null);
+      this.setData({
+        bannerList: result.records || []
+      });
+    } catch (error) {
+      console.error('加载轮播图失败:', error);
+    }
+  },
+
+  // 加载今日推荐数据
+  async loadRecommended() {
+    try {
+      const size = this.data.recommendedColumns === 2 ? 4 : 6; // 每行2个显示4个,每行3个显示6个
+      const recommended = await contentApi.getRecommended(size);
+      this.setData({
+        recommendedList: recommended || []
+      });
+    } catch (error) {
+      console.error('加载今日推荐失败:', error);
+    }
+  },
+
+  // 加载内容列表
+  async loadContentList(isLoadMore = false) {
+    if (this.data.loading) return;
+    
+    this.setData({ loading: true });
+
+    try {
+      const current = isLoadMore ? this.data.current + 1 : 1;
+      const params = {
+        current,
+        size: this.data.size,
+        categoryId: this.data.currentCategory === 0 ? null : this.data.currentCategory,
+        contentType: this.data.contentType,
+        keyword: this.data.keyword || null
+      };
+
+      const result = await contentApi.getContentList(
+        params.current,
+        params.size,
+        params.keyword || '',
+        params.contentType,
+        params.categoryId
+      );
+
+      const contentList = isLoadMore 
+        ? [...this.data.contentList, ...result.records]
+        : result.records;
+
+      this.setData({
+        contentList,
+        current: result.current,
+        total: result.total,
+        hasMore: contentList.length < result.total,
+        loading: false
+      });
+    } catch (error) {
+      wx.showToast({
+        title: error || '加载失败',
+        icon: 'none'
+      });
+      this.setData({ loading: false });
+    }
+  },
+
+  // 切换分类
+  onCategoryTap(e) {
+    const categoryId = e.currentTarget.dataset.id;
+    if (categoryId === this.data.currentCategory) return;
+    
+    this.setData({
+      currentCategory: categoryId,
+      current: 1
+    });
+    this.loadContentList();
+  },
+
+  // 切换内容类型
+  onContentTypeTap(e) {
+    const type = e.currentTarget.dataset.type;
+    // 将字符串 "null" 转换为真正的 null
+    const contentType = type === 'null' ? null : parseInt(type);
+    if (contentType === this.data.contentType) return;
+    
+    this.setData({
+      contentType: contentType,
+      current: 1
+    });
+    this.loadContentList();
+  },
+
+  // 搜索
+  onSearchInput(e) {
+    this.setData({
+      keyword: e.detail.value
+    });
+  },
+
+  onSearchConfirm() {
+    this.setData({ current: 1 });
+    this.loadContentList();
+  },
+
+  // 显示/隐藏搜索框
+  toggleSearch() {
+    const newShowSearch = !this.data.showSearch;
+    this.setData({
+      showSearch: newShowSearch,
+      keyword: '',
+      current: 1
+    });
+    // 动态调整轮播图的margin-top
+    if (newShowSearch) {
+      // 搜索框显示时,轮播图需要再向下移动
+      this.setData({
+        swiperMarginTop: 'calc(200rpx + env(safe-area-inset-top))'
+      });
+    } else {
+      // 搜索框隐藏时,恢复原来的位置
+      this.setData({
+        swiperMarginTop: 'calc(130rpx + env(safe-area-inset-top))'
+      });
+      this.loadContentList();
+    }
+  },
+
+  // 跳转到详情页
+  goToDetail(e) {
+    const contentId = e.currentTarget.dataset.id;
+    wx.navigateTo({
+      url: `/pages/detail/detail?id=${contentId}`
+    });
+  },
+
+  // 上拉加载更多
+  onReachBottom() {
+    if (this.data.hasMore && !this.data.loading) {
+      this.loadContentList(true);
+    }
+  },
+
+  // 下拉刷新
+  onPullDownRefresh() {
+    this.setData({ current: 1 });
+    this.loadContentList().finally(() => {
+      wx.stopPullDownRefresh();
+    });
+  }
+});

+ 6 - 0
pages/index/index.json

@@ -0,0 +1,6 @@
+{
+  "navigationBarTitleText": "首页",
+  "enablePullDownRefresh": true,
+  "navigationStyle": "default",
+  "backgroundColor": "#667eea"
+}

+ 164 - 0
pages/index/index.wxml

@@ -0,0 +1,164 @@
+<!--pages/index/index.wxml-->
+<view class="container">
+  <!-- 搜索栏(固定在顶部) -->
+  <view class="search-bar">
+    <view class="search-box" bindtap="toggleSearch">
+      <icon type="search" size="16" color="#999"></icon>
+      <text class="search-placeholder">搜索书籍或听书</text>
+    </view>
+  </view>
+
+  <!-- 轮播图 -->
+  <view class="swiper-section" style="margin-top: {{swiperMarginTop}};">
+    <swiper 
+      class="swiper" 
+      indicator-dots="{{true}}" 
+      autoplay="{{true}}" 
+      interval="{{3000}}" 
+      duration="{{500}}"
+      circular="{{true}}"
+      indicator-color="rgba(255, 255, 255, 0.5)"
+      indicator-active-color="#fff"
+    >
+      <swiper-item wx:for="{{bannerList}}" wx:key="contentId">
+        <view class="swiper-item" bindtap="goToDetail" data-id="{{item.contentId}}">
+          <image class="swiper-image" src="{{item.coverUrl}}" mode="aspectFill" lazy-load="{{true}}"></image>
+          <view class="swiper-content">
+            <view class="swiper-title">{{item.title}}</view>
+            <view class="swiper-author">{{item.author || '未知作者'}}</view>
+          </view>
+        </view>
+      </swiper-item>
+    </swiper>
+  </view>
+
+  <!-- 搜索输入框 -->
+  <view class="search-input-box" wx:if="{{showSearch}}">
+    <input 
+      class="search-input" 
+      type="text" 
+      placeholder="请输入关键词"
+      value="{{keyword}}"
+      bindinput="onSearchInput"
+      bindconfirm="onSearchConfirm"
+      confirm-type="search"
+    />
+    <text class="search-cancel" bindtap="toggleSearch">取消</text>
+  </view>
+
+  <!-- 内容类型切换 -->
+  <view class="type-tabs">
+    <view 
+      class="type-tab {{contentType === null ? 'active' : ''}}"
+      data-type="null"
+      bindtap="onContentTypeTap"
+    >
+      全部
+    </view>
+    <view 
+      class="type-tab {{contentType === 1 ? 'active' : ''}}"
+      data-type="1"
+      bindtap="onContentTypeTap"
+    >
+      电子书
+    </view>
+    <view 
+      class="type-tab {{contentType === 2 ? 'active' : ''}}"
+      data-type="2"
+      bindtap="onContentTypeTap"
+    >
+      听书
+    </view>
+  </view>
+
+  <!-- 分类列表 -->
+  <scroll-view class="category-scroll" scroll-x="true">
+    <view class="category-list">
+      <view 
+        class="category-item {{currentCategory === item.categoryId ? 'active' : ''}}"
+        wx:for="{{categories}}"
+        wx:key="categoryId"
+        data-id="{{item.categoryId}}"
+        bindtap="onCategoryTap"
+      >
+        {{item.categoryName}}
+      </view>
+    </view>
+  </scroll-view>
+
+  <!-- 今日推荐 -->
+  <view class="recommended-section" wx:if="{{recommendedList.length > 0}}">
+    <view class="section-header">
+      <text class="section-title">今日推荐</text>
+      <text class="section-more">更多 ></text>
+    </view>
+    <view class="recommended-grid {{recommendedColumns === 3 ? 'columns-3' : ''}}">
+      <view 
+        class="recommended-item"
+        wx:for="{{recommendedList}}"
+        wx:key="contentId"
+        data-id="{{item.contentId}}"
+        bindtap="goToDetail"
+      >
+        <view class="recommended-cover-wrapper">
+          <image 
+            class="recommended-cover" 
+            src="{{item.coverUrl}}" 
+            mode="aspectFill" 
+            lazy-load="{{true}}"
+            wx:if="{{item.coverUrl}}"
+          ></image>
+          <view class="recommended-cover-placeholder" wx:else>
+            <text class="recommended-cover-text">{{item.title}}</text>
+          </view>
+        </view>
+        <view class="recommended-title">{{item.title}}</view>
+      </view>
+    </view>
+  </view>
+
+  <!-- 内容列表 -->
+  <view class="content-list">
+    <view 
+      class="content-item"
+      wx:for="{{contentList}}"
+      wx:key="contentId"
+      data-id="{{item.contentId}}"
+      bindtap="goToDetail"
+    >
+      <view class="cover-wrapper">
+        <image 
+          class="cover" 
+          src="{{item.coverUrl}}" 
+          mode="aspectFill" 
+          lazy-load="{{true}}"
+          wx:if="{{item.coverUrl}}"
+        ></image>
+        <view class="cover-placeholder" wx:else>
+          <text class="cover-text">{{item.title}}</text>
+        </view>
+      </view>
+      <view class="content-info">
+        <view class="title">{{item.title}}</view>
+        <view class="author">{{item.author || '未知作者'}}</view>
+        <view class="meta">
+          <text class="type">{{item.contentType === 1 ? '电子书' : '听书'}}</text>
+          <text class="chapters">{{item.totalChapters || 0}}章</text>
+          <text class="status">{{item.status === 1 ? '连载中' : '已完结'}}</text>
+        </view>
+        <view class="description">{{item.description || '暂无简介'}}</view>
+      </view>
+    </view>
+
+    <!-- 加载状态 -->
+    <view class="loading" wx:if="{{loading}}">
+      <text>加载中...</text>
+    </view>
+    <view class="no-more" wx:if="{{!hasMore && !loading && contentList.length > 0}}">
+      <text>没有更多了</text>
+    </view>
+    <view class="empty" wx:if="{{!loading && contentList.length === 0}}">
+      <text>暂无内容</text>
+    </view>
+  </view>
+</view>

+ 546 - 0
pages/index/index.wxss

@@ -0,0 +1,546 @@
+/* pages/index/index.wxss */
+page {
+  padding: 0;
+  margin: 0;
+}
+
+.container {
+  min-height: 100vh;
+  background: linear-gradient(180deg, #f8f9ff 0%, #f5f5f5 100%);
+  padding-bottom: 20rpx;
+  padding-top: 0;
+  margin-top: 0;
+}
+
+/* 轮播图区域 */
+.swiper-section {
+  width: 100%;
+  height: 400rpx;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  position: relative;
+  overflow: hidden;
+  padding-top: 0;
+  transition: margin-top 0.3s ease;
+}
+
+.swiper {
+  width: 100%;
+  height: 100%;
+}
+
+.swiper-item {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  overflow: hidden;
+}
+
+.swiper-image {
+  width: 100%;
+  height: 100%;
+  opacity: 0.7;
+}
+
+.swiper-content {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  padding: 40rpx 30rpx 30rpx;
+  background: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.6) 100%);
+  color: #fff;
+}
+
+.swiper-title {
+  font-size: 36rpx;
+  font-weight: 600;
+  margin-bottom: 12rpx;
+  text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.swiper-author {
+  font-size: 26rpx;
+  opacity: 0.9;
+  text-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.3);
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.search-bar {
+  background: #fff;
+  padding: 20rpx 30rpx 20rpx;
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  z-index: 1000;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
+  padding-top: calc(20rpx + env(safe-area-inset-top));
+}
+
+.search-box {
+  display: flex;
+  align-items: center;
+  background: linear-gradient(135deg, #f5f5f5 0%, #f0f0f0 100%);
+  border-radius: 50rpx;
+  padding: 22rpx 32rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+  transition: all 0.3s ease;
+  border: 2rpx solid transparent;
+}
+
+.search-box:active {
+  transform: scale(0.98);
+  box-shadow: 0 2rpx 12rpx rgba(102, 126, 234, 0.2);
+  border-color: #667eea;
+}
+
+.search-placeholder {
+  margin-left: 12rpx;
+  font-size: 28rpx;
+  color: #999;
+  flex: 1;
+}
+
+.search-input-box {
+  display: flex;
+  align-items: center;
+  background: rgba(255, 255, 255, 0.98);
+  padding: 20rpx 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
+  position: fixed;
+  top: calc(100rpx + env(safe-area-inset-top));
+  left: 0;
+  right: 0;
+  z-index: 999;
+}
+
+.search-input {
+  flex: 1;
+  height: 64rpx;
+  padding: 0 24rpx;
+  background: #f5f5f5;
+  border-radius: 32rpx;
+  font-size: 28rpx;
+  border: 2rpx solid transparent;
+  transition: all 0.3s ease;
+}
+
+.search-input:focus {
+  border-color: #667eea;
+  background: #fff;
+}
+
+.search-cancel {
+  margin-left: 20rpx;
+  font-size: 28rpx;
+  color: #667eea;
+  font-weight: 500;
+  padding: 8rpx 16rpx;
+}
+
+.type-tabs {
+  display: flex;
+  background: #fff;
+  padding: 24rpx 30rpx;
+  border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
+}
+
+.type-tab {
+  margin-right: 48rpx;
+  font-size: 30rpx;
+  color: #666;
+  padding: 12rpx 0;
+  position: relative;
+  font-weight: 500;
+  transition: all 0.3s ease;
+}
+
+.type-tab.active {
+  color: #667eea;
+  font-weight: 600;
+}
+
+.type-tab.active::after {
+  content: '';
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 6rpx;
+  background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
+  border-radius: 3rpx;
+  animation: slideIn 0.3s ease;
+}
+
+@keyframes slideIn {
+  from {
+    transform: scaleX(0);
+  }
+  to {
+    transform: scaleX(1);
+  }
+}
+
+.category-scroll {
+  white-space: nowrap;
+  background: #fff;
+  padding: 24rpx 0;
+  border-bottom: 1rpx solid rgba(0, 0, 0, 0.05);
+}
+
+.category-list {
+  display: inline-flex;
+  padding: 0 30rpx;
+  gap: 20rpx;
+}
+
+.category-item {
+  display: inline-block;
+  padding: 12rpx 32rpx;
+  font-size: 26rpx;
+  color: #666;
+  background: linear-gradient(135deg, #f5f5f5 0%, #f0f0f0 100%);
+  border-radius: 32rpx;
+  font-weight: 500;
+  transition: all 0.3s ease;
+  white-space: nowrap;
+  border: 2rpx solid transparent;
+}
+
+.category-item.active {
+  color: #fff;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.3);
+  transform: scale(1.05);
+  border-color: rgba(255, 255, 255, 0.3);
+}
+
+/* 今日推荐区域 */
+.recommended-section {
+  background: #fff;
+  margin: 24rpx 0;
+  border-radius: 0;
+  padding: 30rpx 20rpx;
+  box-shadow: 0 4rpx 20rpx rgba(102, 126, 234, 0.08);
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 24rpx;
+}
+
+.section-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #1a1a1a;
+}
+
+.section-more {
+  font-size: 26rpx;
+  color: #999;
+}
+
+.recommended-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20rpx;
+}
+
+.recommended-item {
+  flex: 0 0 calc(50% - 10rpx); /* 每行2个,默认 */
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  transition: transform 0.3s ease;
+}
+
+.recommended-item:active {
+  transform: scale(0.95);
+}
+
+.recommended-cover-wrapper {
+  width: 100%;
+  aspect-ratio: 3/4;
+  border-radius: 12rpx;
+  overflow: hidden;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
+  margin-bottom: 12rpx;
+  background: linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%);
+}
+
+.recommended-cover {
+  width: 100%;
+  height: 100%;
+  transition: transform 0.3s ease;
+}
+
+.recommended-item:active .recommended-cover {
+  transform: scale(1.05);
+}
+
+.recommended-cover-placeholder {
+  width: 100%;
+  height: 100%;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 12rpx;
+  box-sizing: border-box;
+}
+
+.recommended-cover-text {
+  color: #fff;
+  font-size: 20rpx;
+  font-weight: 500;
+  text-align: center;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  line-height: 1.4;
+}
+
+.recommended-title {
+  font-size: 24rpx;
+  color: #333;
+  font-weight: 500;
+  text-align: center;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  width: 100%;
+  line-height: 1.4;
+}
+
+/* 每行3个的样式 */
+.recommended-grid.columns-3 .recommended-item {
+  flex: 0 0 calc(33.333% - 14rpx);
+}
+
+.content-list {
+  padding: 30rpx 24rpx;
+}
+
+.content-item {
+  display: flex;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 32rpx;
+  margin-bottom: 24rpx;
+  box-shadow: 0 4rpx 20rpx rgba(102, 126, 234, 0.08);
+  transition: all 0.3s ease;
+  position: relative;
+  overflow: hidden;
+}
+
+.content-item::before {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 6rpx;
+  height: 100%;
+  background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
+  opacity: 0;
+  transition: opacity 0.3s ease;
+}
+
+.content-item:active {
+  transform: translateY(-4rpx);
+  box-shadow: 0 8rpx 28rpx rgba(102, 126, 234, 0.15);
+}
+
+.content-item:active::before {
+  opacity: 1;
+}
+
+.cover-wrapper {
+  width: 180rpx;
+  height: 240rpx;
+  margin-right: 32rpx;
+  flex-shrink: 0;
+  border-radius: 12rpx;
+  overflow: hidden;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
+}
+
+.cover {
+  width: 100%;
+  height: 100%;
+  transition: transform 0.3s ease;
+}
+
+.content-item:active .cover {
+  transform: scale(1.05);
+}
+
+.cover-placeholder {
+  width: 100%;
+  height: 100%;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 12rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 12rpx;
+  box-sizing: border-box;
+}
+
+.cover-text {
+  color: #fff;
+  font-size: 22rpx;
+  font-weight: 500;
+  text-align: center;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 3;
+  -webkit-box-orient: vertical;
+  line-height: 1.4;
+}
+
+.content-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  min-width: 0;
+}
+
+.title {
+  font-size: 34rpx;
+  font-weight: 600;
+  color: #1a1a1a;
+  margin-bottom: 12rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  line-height: 1.4;
+}
+
+.author {
+  font-size: 26rpx;
+  color: #8a8a8a;
+  margin-bottom: 14rpx;
+  font-weight: 500;
+}
+
+.meta {
+  display: flex;
+  align-items: center;
+  margin-bottom: 16rpx;
+  flex-wrap: wrap;
+  gap: 12rpx;
+}
+
+.meta text {
+  font-size: 22rpx;
+  color: #667eea;
+  padding: 6rpx 14rpx;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
+  border-radius: 12rpx;
+  font-weight: 500;
+}
+
+.description {
+  font-size: 26rpx;
+  color: #666;
+  line-height: 1.8;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+}
+
+.loading {
+  text-align: center;
+  padding: 60rpx 40rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.loading text {
+  font-size: 26rpx;
+  color: #999;
+  margin-top: 20rpx;
+}
+
+.loading::before {
+  content: '';
+  width: 40rpx;
+  height: 40rpx;
+  border: 4rpx solid #f0f0f0;
+  border-top-color: #667eea;
+  border-radius: 50%;
+  animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+.no-more {
+  text-align: center;
+  padding: 40rpx;
+  font-size: 26rpx;
+  color: #999;
+  position: relative;
+}
+
+.no-more::before,
+.no-more::after {
+  content: '';
+  position: absolute;
+  top: 50%;
+  width: 80rpx;
+  height: 1rpx;
+  background: #e0e0e0;
+}
+
+.no-more::before {
+  left: 20%;
+}
+
+.no-more::after {
+  right: 20%;
+}
+
+.empty {
+  text-align: center;
+  padding: 200rpx 40rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.empty::before {
+  content: '📖';
+  font-size: 120rpx;
+  margin-bottom: 30rpx;
+  opacity: 0.3;
+}
+
+.empty text {
+  font-size: 28rpx;
+  color: #999;
+  line-height: 1.6;
+}

+ 94 - 0
pages/login/login.js

@@ -0,0 +1,94 @@
+// pages/login/login.js
+const userApi = require('../../api/user');
+const userUtil = require('../../utils/user');
+
+Page({
+  data: {
+    username: '',
+    password: '',
+    loading: false
+  },
+
+  onLoad() {
+    // 如果已登录,跳转到首页
+    if (userUtil.isLogin()) {
+      const userInfo = userUtil.getUserInfo();
+      // 如果已登录的是管理员,清除登录信息并提示
+      if (userInfo.userRole === 1) {
+        userUtil.clearUserInfo();
+        wx.showToast({
+          title: '管理员请使用Web端登录',
+          icon: 'none',
+          duration: 2000
+        });
+        return;
+      }
+      // 普通用户直接跳转到首页
+      wx.reLaunch({
+        url: '/pages/index/index'
+      });
+    }
+  },
+
+  // 输入用户名
+  onUsernameInput(e) {
+    this.setData({
+      username: e.detail.value
+    });
+  },
+
+  // 输入密码
+  onPasswordInput(e) {
+    this.setData({
+      password: e.detail.value
+    });
+  },
+
+  // 登录
+  async handleLogin() {
+    const { username, password } = this.data;
+    
+    if (!username || !password) {
+      wx.showToast({
+        title: '请输入用户名和密码',
+        icon: 'none'
+      });
+      return;
+    }
+
+    this.setData({ loading: true });
+
+    try {
+      const result = await userApi.userLogin(username, password);
+      // result包含token和user
+      // 后端已禁止管理员登录,这里只会是普通用户
+      userUtil.saveUserInfo(result.user, result.token);
+      
+      wx.showToast({
+        title: '登录成功',
+        icon: 'success'
+      });
+
+      setTimeout(() => {
+        wx.reLaunch({
+          url: '/pages/index/index'
+        });
+      }, 1500);
+    } catch (error) {
+      wx.showToast({
+        title: error || '登录失败',
+        icon: 'none',
+        duration: 2000
+      });
+    } finally {
+      this.setData({ loading: false });
+    }
+  },
+
+  // 跳转到注册页
+  goToRegister() {
+    wx.navigateTo({
+      url: '/pages/register/register'
+    });
+  }
+});

+ 4 - 0
pages/login/login.json

@@ -0,0 +1,4 @@
+{
+  "navigationBarTitleText": "登录",
+  "navigationStyle": "custom"
+}

+ 42 - 0
pages/login/login.wxml

@@ -0,0 +1,42 @@
+<!--pages/login/login.wxml-->
+<view class="container">
+  <view class="login-box">
+    <view class="title">听书阅读</view>
+    <view class="subtitle">欢迎回来</view>
+    
+    <view class="form">
+      <view class="input-group">
+        <input 
+          class="input" 
+          type="text" 
+          placeholder="请输入用户名"
+          value="{{username}}"
+          bindinput="onUsernameInput"
+        />
+      </view>
+      
+      <view class="input-group">
+        <input 
+          class="input" 
+          type="password" 
+          placeholder="请输入密码"
+          value="{{password}}"
+          bindinput="onPasswordInput"
+        />
+      </view>
+      
+      <button 
+        class="login-btn" 
+        bindtap="handleLogin"
+        loading="{{loading}}"
+        disabled="{{loading}}"
+      >
+        登录
+      </button>
+      
+      <view class="register-link">
+        <text bindtap="goToRegister">还没有账号?立即注册</text>
+      </view>
+    </view>
+  </view>
+</view>

+ 86 - 0
pages/login/login.wxss

@@ -0,0 +1,86 @@
+/* pages/login/login.wxss */
+.container {
+  min-height: 100vh;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 40rpx;
+}
+
+.login-box {
+  width: 100%;
+  max-width: 600rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 60rpx 40rpx;
+  box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.1);
+}
+
+.title {
+  font-size: 48rpx;
+  font-weight: bold;
+  color: #333;
+  text-align: center;
+  margin-bottom: 20rpx;
+}
+
+.subtitle {
+  font-size: 28rpx;
+  color: #999;
+  text-align: center;
+  margin-bottom: 60rpx;
+}
+
+.form {
+  width: 100%;
+}
+
+.input-group {
+  margin-bottom: 30rpx;
+}
+
+.input {
+  width: 100%;
+  height: 88rpx;
+  padding: 0 30rpx;
+  border: 2rpx solid #e0e0e0;
+  border-radius: 10rpx;
+  font-size: 28rpx;
+  box-sizing: border-box;
+}
+
+.input:focus {
+  border-color: #667eea;
+}
+
+.login-btn {
+  width: 100%;
+  height: 88rpx;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  border-radius: 10rpx;
+  font-size: 32rpx;
+  font-weight: bold;
+  margin-top: 40rpx;
+  border: none;
+}
+
+.login-btn::after {
+  border: none;
+}
+
+.login-btn[disabled] {
+  opacity: 0.6;
+}
+
+.register-link {
+  text-align: center;
+  margin-top: 40rpx;
+  font-size: 26rpx;
+  color: #667eea;
+}
+
+.register-link text {
+  text-decoration: underline;
+}

+ 18 - 0
pages/logs/logs.js

@@ -0,0 +1,18 @@
+// logs.js
+const util = require('../../utils/util.js')
+
+Page({
+  data: {
+    logs: []
+  },
+  onLoad() {
+    this.setData({
+      logs: (wx.getStorageSync('logs') || []).map(log => {
+        return {
+          date: util.formatTime(new Date(log)),
+          timeStamp: log
+        }
+      })
+    })
+  }
+})

+ 4 - 0
pages/logs/logs.json

@@ -0,0 +1,4 @@
+{
+  "usingComponents": {
+  }
+}

+ 6 - 0
pages/logs/logs.wxml

@@ -0,0 +1,6 @@
+<!--logs.wxml-->
+<scroll-view class="scrollarea" scroll-y type="list">
+  <block wx:for="{{logs}}" wx:key="timeStamp" wx:for-item="log">
+    <view class="log-item">{{index + 1}}. {{log.date}}</view>
+  </block>
+</scroll-view>

+ 16 - 0
pages/logs/logs.wxss

@@ -0,0 +1,16 @@
+page {
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+}
+.scrollarea {
+  flex: 1;
+  overflow-y: hidden;
+}
+.log-item {
+  margin-top: 20rpx;
+  text-align: center;
+}
+.log-item:last-child {
+  padding-bottom: env(safe-area-inset-bottom);
+}

+ 102 - 0
pages/profile/profile.js

@@ -0,0 +1,102 @@
+// pages/profile/profile.js
+const userUtil = require('../../utils/user');
+const userApi = require('../../api/user');
+
+Page({
+  data: {
+    userInfo: null,
+    systemInfo: null,
+    pageHeight: 0
+  },
+
+  onLoad() {
+    this.loadUserInfo();
+    this.setPageHeight();
+  },
+
+  onShow() {
+    this.loadUserInfo();
+  },
+
+  // 设置页面高度,匹配手机屏幕
+  setPageHeight() {
+    try {
+      const systemInfo = wx.getSystemInfoSync();
+      const windowHeight = systemInfo.windowHeight; // 窗口高度(px),已自动减去tabBar高度
+      const windowWidth = systemInfo.windowWidth; // 窗口宽度(px)
+      
+      // 小程序中,windowHeight已经自动减去了tabBar高度
+      // 转换为rpx:rpx = px * (750 / 屏幕宽度)
+      const rpxRatio = 750 / windowWidth;
+      const pageHeightRpx = windowHeight * rpxRatio;
+      
+      this.setData({
+        systemInfo: systemInfo,
+        pageHeight: Math.floor(pageHeightRpx)
+      });
+      
+      console.log('页面高度设置:', {
+        windowWidth: windowWidth + 'px',
+        windowHeight: windowHeight + 'px',
+        rpxRatio: rpxRatio.toFixed(4),
+        pageHeight: Math.floor(pageHeightRpx) + 'rpx'
+      });
+    } catch (error) {
+      console.error('设置页面高度失败:', error);
+      // 如果获取系统信息失败,使用默认值
+      this.setData({
+        pageHeight: 0 // 使用CSS的100vh作为fallback
+      });
+    }
+  },
+
+  // 加载用户信息
+  loadUserInfo() {
+    const userInfo = userUtil.getUserInfo();
+    this.setData({ userInfo });
+  },
+
+  // 退出登录
+  logout() {
+    wx.showModal({
+      title: '提示',
+      content: '确定要退出登录吗?',
+      success: (res) => {
+        if (res.confirm) {
+          userUtil.clearUserInfo();
+          wx.reLaunch({
+            url: '/pages/login/login'
+          });
+        }
+      }
+    });
+  },
+
+  // 跳转到书架
+  goToBookshelf() {
+    wx.switchTab({
+      url: '/pages/bookshelf/bookshelf'
+    });
+  },
+
+  // 跳转到历史记录
+  goToHistory() {
+    wx.navigateTo({
+      url: '/pages/history/history'
+    });
+  },
+
+  // 跳转到AI客服
+  goToChat() {
+    wx.navigateTo({
+      url: '/pages/chat/chat'
+    });
+  },
+
+  // 跳转到设置
+  goToSettings() {
+    wx.navigateTo({
+      url: '/pages/settings/settings'
+    });
+  }
+});

+ 3 - 0
pages/profile/profile.json

@@ -0,0 +1,3 @@
+{
+  "navigationBarTitleText": "我的"
+}

+ 45 - 0
pages/profile/profile.wxml

@@ -0,0 +1,45 @@
+<!--pages/profile/profile.wxml-->
+<view class="container" style="{{pageHeight ? 'height: ' + pageHeight + 'rpx;' : ''}}">
+  <!-- 用户信息 -->
+  <view class="user-section">
+    <view class="user-header">
+      <image class="avatar" src="{{userInfo.avatarUrl || 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'}}" mode="aspectFill"></image>
+      <view class="user-info">
+        <view class="nickname-row">
+          <text class="nickname">{{userInfo.nickname || userInfo.username || '用户'}}</text>
+          <text class="vip-badge" wx:if="{{userInfo.memberLevel === 1}}">VIP</text>
+        </view>
+        <view class="username">用户名:{{userInfo.username}}</view>
+      </view>
+    </view>
+  </view>
+
+  <!-- 功能菜单 -->
+  <view class="menu-section">
+    <view class="menu-item" bindtap="goToBookshelf">
+      <text class="menu-icon">📚</text>
+      <text class="menu-text">我的书架</text>
+      <text class="menu-arrow">></text>
+    </view>
+    <view class="menu-item" bindtap="goToHistory">
+      <text class="menu-icon">📖</text>
+      <text class="menu-text">阅读历史</text>
+      <text class="menu-arrow">></text>
+    </view>
+    <view class="menu-item" bindtap="goToChat">
+      <text class="menu-icon">💬</text>
+      <text class="menu-text">老舅AI智能客服</text>
+      <text class="menu-arrow">></text>
+    </view>
+    <view class="menu-item" bindtap="goToSettings">
+      <text class="menu-icon">⚙️</text>
+      <text class="menu-text">设置</text>
+      <text class="menu-arrow">></text>
+    </view>
+  </view>
+
+  <!-- 退出登录 -->
+  <view class="logout-section">
+    <button class="logout-btn" bindtap="logout">退出登录</button>
+  </view>
+</view>

+ 233 - 0
pages/profile/profile.wxss

@@ -0,0 +1,233 @@
+/* pages/profile/profile.wxss */
+page {
+  height: 100%;
+  width: 100%;
+  background: linear-gradient(180deg, #f8f9ff 0%, #f5f5f5 100%);
+}
+
+.container {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  /* 高度由JS动态设置,如果没有设置则使用100vh作为fallback */
+  min-height: 100vh;
+  height: 100vh;
+  background: linear-gradient(180deg, #f8f9ff 0%, #f5f5f5 100%);
+  box-sizing: border-box;
+  /* 确保内容在安全区域内 */
+  padding: 0;
+  margin: 0;
+}
+
+.user-section {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  padding: 40rpx 30rpx 50rpx;
+  padding-top: calc(40rpx + env(safe-area-inset-top));
+  color: #fff;
+  position: relative;
+  overflow: hidden;
+  flex-shrink: 0;
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.user-section::before {
+  content: '';
+  position: absolute;
+  top: -50%;
+  right: -20%;
+  width: 400rpx;
+  height: 400rpx;
+  background: rgba(255, 255, 255, 0.1);
+  border-radius: 50%;
+  pointer-events: none;
+}
+
+.user-section::after {
+  content: '';
+  position: absolute;
+  bottom: -30%;
+  left: -10%;
+  width: 300rpx;
+  height: 300rpx;
+  background: rgba(255, 255, 255, 0.08);
+  border-radius: 50%;
+  pointer-events: none;
+}
+
+.user-header {
+  display: flex;
+  align-items: center;
+  position: relative;
+  z-index: 1;
+}
+
+.avatar {
+  width: 140rpx;
+  height: 140rpx;
+  border-radius: 50%;
+  margin-right: 32rpx;
+  border: 6rpx solid rgba(255, 255, 255, 0.4);
+  flex-shrink: 0;
+  box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2);
+  transition: transform 0.3s ease;
+}
+
+.avatar:active {
+  transform: scale(0.95);
+}
+
+.user-info {
+  flex: 1;
+  min-width: 0;
+}
+
+.nickname-row {
+  display: flex;
+  align-items: center;
+  margin-bottom: 16rpx;
+  gap: 16rpx;
+  flex-wrap: wrap;
+}
+
+.nickname {
+  font-size: 40rpx;
+  font-weight: 600;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
+}
+
+.username {
+  font-size: 26rpx;
+  opacity: 0.95;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-weight: 500;
+}
+
+.vip-badge {
+  display: inline-block;
+  padding: 6rpx 18rpx;
+  background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
+  color: #333;
+  border-radius: 24rpx;
+  font-size: 24rpx;
+  font-weight: bold;
+  flex-shrink: 0;
+  box-shadow: 0 4rpx 12rpx rgba(255, 215, 0, 0.4);
+  border: 2rpx solid rgba(255, 255, 255, 0.3);
+}
+
+.menu-section {
+  background: #fff;
+  margin: 0;
+  border-radius: 0;
+  overflow: hidden;
+  box-shadow: none;
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+}
+
+.menu-item {
+  display: flex;
+  align-items: center;
+  padding: 36rpx 30rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+  transition: all 0.3s ease;
+  position: relative;
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.menu-item::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 0;
+  bottom: 0;
+  width: 6rpx;
+  background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
+  opacity: 0;
+  transition: opacity 0.3s ease;
+}
+
+.menu-item:active {
+  background: linear-gradient(90deg, rgba(102, 126, 234, 0.05) 0%, transparent 100%);
+  padding-left: 36rpx;
+}
+
+.menu-item:active::before {
+  opacity: 1;
+}
+
+.menu-item:last-child {
+  border-bottom: none;
+}
+
+.menu-icon {
+  font-size: 44rpx;
+  margin-right: 24rpx;
+  width: 50rpx;
+  text-align: center;
+  transition: transform 0.3s ease;
+}
+
+.menu-item:active .menu-icon {
+  transform: scale(1.1);
+}
+
+.menu-text {
+  flex: 1;
+  font-size: 30rpx;
+  color: #1a1a1a;
+  font-weight: 500;
+}
+
+.menu-arrow {
+  font-size: 28rpx;
+  color: #999;
+  transition: transform 0.3s ease;
+}
+
+.menu-item:active .menu-arrow {
+  transform: translateX(4rpx);
+  color: #667eea;
+}
+
+.logout-section {
+  padding: 30rpx 0 40rpx;
+  margin-top: auto; /* 将退出登录按钮推到底部,铺满高度 */
+  flex-shrink: 0;
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.logout-btn {
+  width: calc(100% - 60rpx);
+  margin: 0 30rpx;
+  height: 96rpx;
+  line-height: 96rpx;
+  background: #fff;
+  color: #ff4444;
+  border-radius: 16rpx;
+  font-size: 32rpx;
+  font-weight: 500;
+  border: 2rpx solid #ffe0e0;
+  transition: all 0.3s ease;
+  box-shadow: 0 4rpx 16rpx rgba(255, 68, 68, 0.1);
+}
+
+.logout-btn::after {
+  border: none;
+}
+
+.logout-btn:active {
+  background: linear-gradient(135deg, #ffe0e0 0%, #fff5f5 100%);
+  transform: scale(0.98);
+  box-shadow: 0 2rpx 12rpx rgba(255, 68, 68, 0.15);
+}

+ 159 - 0
pages/read/read.js

@@ -0,0 +1,159 @@
+// pages/read/read.js
+const chapterApi = require('../../api/chapter');
+const historyApi = require('../../api/history');
+const userUtil = require('../../utils/user');
+
+Page({
+  data: {
+    contentId: null,
+    chapterId: null,
+    chapter: null,
+    chapters: [],
+    currentIndex: 0,
+    fontSize: 32, // 字体大小(rpx)
+    backgroundColor: '#fff', // 背景颜色
+    loading: true,
+    showSettings: false
+  },
+
+  onLoad(options) {
+    const contentId = parseInt(options.contentId);
+    const chapterId = parseInt(options.chapterId);
+    
+    if (!contentId || !chapterId) {
+      wx.showToast({
+        title: '参数错误',
+        icon: 'none'
+      });
+      setTimeout(() => {
+        wx.navigateBack();
+      }, 1500);
+      return;
+    }
+
+    this.setData({ contentId, chapterId });
+    this.loadChapters();
+    this.loadChapter();
+  },
+
+  // 加载章节列表
+  async loadChapters() {
+    try {
+      const chapters = await chapterApi.getBookChapterList(this.data.contentId);
+      const currentIndex = chapters.findIndex(c => c.chapterId === this.data.chapterId);
+      this.setData({ 
+        chapters,
+        currentIndex: currentIndex >= 0 ? currentIndex : 0
+      });
+    } catch (error) {
+      console.error('加载章节列表失败:', error);
+    }
+  },
+
+  // 加载章节内容
+  async loadChapter() {
+    this.setData({ loading: true });
+    try {
+      const chapter = await chapterApi.getBookChapter(this.data.chapterId);
+      this.setData({ chapter });
+      wx.setNavigationBarTitle({
+        title: chapter.chapterTitle || '阅读'
+      });
+      
+      // 保存阅读历史
+      this.saveHistory();
+    } catch (error) {
+      wx.showToast({
+        title: error || '加载失败',
+        icon: 'none'
+      });
+    } finally {
+      this.setData({ loading: false });
+    }
+  },
+  
+  // 保存阅读历史
+  async saveHistory() {
+    if (!userUtil.isLogin() || !this.data.contentId) {
+      return;
+    }
+    try {
+      await historyApi.addHistory(this.data.contentId);
+    } catch (error) {
+      console.error('保存阅读历史失败:', error);
+      // 静默失败,不影响阅读体验
+    }
+  },
+
+  // 上一章
+  prevChapter() {
+    if (this.data.currentIndex > 0) {
+      const prevChapter = this.data.chapters[this.data.currentIndex - 1];
+      this.setData({
+        chapterId: prevChapter.chapterId,
+        currentIndex: this.data.currentIndex - 1
+      });
+      this.loadChapter();
+    } else {
+      wx.showToast({
+        title: '已经是第一章了',
+        icon: 'none'
+      });
+    }
+  },
+
+  // 下一章
+  nextChapter() {
+    if (this.data.currentIndex < this.data.chapters.length - 1) {
+      const nextChapter = this.data.chapters[this.data.currentIndex + 1];
+      this.setData({
+        chapterId: nextChapter.chapterId,
+        currentIndex: this.data.currentIndex + 1
+      });
+      this.loadChapter();
+    } else {
+      wx.showToast({
+        title: '已经是最后一章了',
+        icon: 'none'
+      });
+    }
+  },
+
+  // 显示/隐藏设置
+  toggleSettings() {
+    this.setData({
+      showSettings: !this.data.showSettings
+    });
+  },
+
+  // 调整字体大小
+  adjustFontSize(e) {
+    const size = parseInt(e.detail.value);
+    this.setData({ fontSize: size * 2 + 28 }); // 28-48
+  },
+
+  // 切换背景色
+  changeBackground(e) {
+    const color = e.currentTarget.dataset.color;
+    this.setData({ backgroundColor: color });
+  },
+
+  // 显示目录
+  showChapterList() {
+    const itemList = this.data.chapters.map((item, index) => 
+      `${index + 1}. ${item.chapterTitle}`
+    );
+    
+    wx.showActionSheet({
+      itemList,
+      success: (res) => {
+        const chapter = this.data.chapters[res.tapIndex];
+        this.setData({
+          chapterId: chapter.chapterId,
+          currentIndex: res.tapIndex
+        });
+        this.loadChapter();
+      }
+    });
+  }
+});

+ 3 - 0
pages/read/read.json

@@ -0,0 +1,3 @@
+{
+  "navigationBarTitleText": "阅读"
+}

+ 85 - 0
pages/read/read.wxml

@@ -0,0 +1,85 @@
+<!--pages/read/read.wxml-->
+<view class="container" style="background-color: {{backgroundColor}}">
+  <view class="content" wx:if="{{!loading && chapter}}">
+    <view class="chapter-title">{{chapter.chapterTitle}}</view>
+    <rich-text 
+      class="chapter-content" 
+      style="font-size: {{fontSize}}rpx;"
+      nodes="{{chapter.contentText}}"
+    ></rich-text>
+  </view>
+
+  <!-- 底部操作栏 -->
+  <view class="toolbar">
+    <view class="tool-item" bindtap="prevChapter">
+      <text>上一章</text>
+    </view>
+    <view class="tool-item" bindtap="showChapterList">
+      <text>目录</text>
+    </view>
+    <view class="tool-item" bindtap="toggleSettings">
+      <text>设置</text>
+    </view>
+    <view class="tool-item" bindtap="nextChapter">
+      <text>下一章</text>
+    </view>
+  </view>
+
+  <!-- 设置面板 -->
+  <view class="settings-panel" wx:if="{{showSettings}}">
+    <view class="settings-title">阅读设置</view>
+    
+    <!-- 字体大小 -->
+    <view class="setting-item">
+      <text class="setting-label">字体大小</text>
+      <view class="font-slider-container">
+        <slider 
+          class="font-slider"
+          min="0"
+          max="10"
+          value="{{(fontSize - 28) / 2}}"
+          step="1"
+          bindchange="adjustFontSize"
+          show-value="{{false}}"
+          activeColor="#667eea"
+          backgroundColor="#e0e0e0"
+        />
+        <text class="font-size-text">{{fontSize}}rpx</text>
+      </view>
+    </view>
+
+    <!-- 背景颜色 -->
+    <view class="setting-item">
+      <text class="setting-label">背景颜色</text>
+      <view class="color-picker">
+        <view 
+          class="color-item {{backgroundColor === '#fff' ? 'active' : ''}}"
+          data-color="#fff"
+          bindtap="changeBackground"
+        ></view>
+        <view 
+          class="color-item {{backgroundColor === '#f5f5dc' ? 'active' : ''}}"
+          data-color="#f5f5dc"
+          bindtap="changeBackground"
+        ></view>
+        <view 
+          class="color-item {{backgroundColor === '#e8f5e9' ? 'active' : ''}}"
+          data-color="#e8f5e9"
+          bindtap="changeBackground"
+        ></view>
+        <view 
+          class="color-item {{backgroundColor === '#263238' ? 'active' : ''}}"
+          data-color="#263238"
+          bindtap="changeBackground"
+        ></view>
+      </view>
+    </view>
+  </view>
+
+  <!-- 遮罩层 -->
+  <view 
+    class="mask" 
+    wx:if="{{showSettings}}"
+    bindtap="toggleSettings"
+  ></view>
+</view>

+ 238 - 0
pages/read/read.wxss

@@ -0,0 +1,238 @@
+/* pages/read/read.wxss */
+.container {
+  min-height: 100vh;
+  padding: 50rpx 40rpx;
+  padding-bottom: 140rpx;
+  transition: background-color 0.3s ease;
+}
+
+.content {
+  min-height: calc(100vh - 250rpx);
+  max-width: 700rpx;
+  margin: 0 auto;
+}
+
+.chapter-title {
+  font-size: 38rpx;
+  font-weight: 600;
+  text-align: center;
+  margin-bottom: 50rpx;
+  padding-bottom: 30rpx;
+  border-bottom: 2rpx solid rgba(0, 0, 0, 0.08);
+  color: #1a1a1a;
+  line-height: 1.5;
+  position: relative;
+}
+
+.chapter-title::after {
+  content: '';
+  position: absolute;
+  bottom: -2rpx;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 60rpx;
+  height: 2rpx;
+  background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
+}
+
+.chapter-content {
+  line-height: 2.2;
+  color: #333;
+  text-align: justify;
+  font-size: 32rpx;
+  letter-spacing: 1rpx;
+  word-spacing: 2rpx;
+}
+
+.toolbar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  display: flex;
+  background: rgba(255, 255, 255, 0.98);
+  backdrop-filter: blur(20rpx);
+  border-top: 1rpx solid rgba(0, 0, 0, 0.08);
+  padding: 24rpx 0;
+  padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
+  box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08);
+  z-index: 10;
+}
+
+.tool-item {
+  flex: 1;
+  text-align: center;
+  font-size: 28rpx;
+  color: #333;
+  padding: 12rpx;
+  font-weight: 500;
+  transition: all 0.3s ease;
+  position: relative;
+}
+
+.tool-item:active {
+  color: #667eea;
+  transform: scale(0.95);
+}
+
+.tool-item::before {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 0;
+  height: 4rpx;
+  background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
+  border-radius: 2rpx;
+  transition: width 0.3s ease;
+}
+
+.tool-item:active::before {
+  width: 60%;
+}
+
+.settings-panel {
+  position: fixed;
+  bottom: 100rpx;
+  left: 0;
+  right: 0;
+  background: rgba(255, 255, 255, 0.98);
+  backdrop-filter: blur(20rpx);
+  padding: 40rpx 30rpx;
+  padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
+  box-shadow: 0 -4rpx 30rpx rgba(0, 0, 0, 0.12);
+  z-index: 100;
+  border-top-left-radius: 24rpx;
+  border-top-right-radius: 24rpx;
+  animation: slideUp 0.3s ease;
+}
+
+@keyframes slideUp {
+  from {
+    transform: translateY(100%);
+  }
+  to {
+    transform: translateY(0);
+  }
+}
+
+.settings-title {
+  font-size: 34rpx;
+  font-weight: 600;
+  margin-bottom: 36rpx;
+  color: #1a1a1a;
+  text-align: center;
+  position: relative;
+  padding-bottom: 20rpx;
+}
+
+.settings-title::after {
+  content: '';
+  position: absolute;
+  bottom: 0;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 60rpx;
+  height: 4rpx;
+  background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
+  border-radius: 2rpx;
+}
+
+.setting-item {
+  margin-bottom: 36rpx;
+}
+
+.setting-item:last-child {
+  margin-bottom: 0;
+}
+
+.setting-label {
+  font-size: 28rpx;
+  color: #666;
+  margin-bottom: 24rpx;
+  display: block;
+  font-weight: 500;
+}
+
+.font-slider-container {
+  display: flex;
+  align-items: center;
+  gap: 20rpx;
+}
+
+.font-slider {
+  flex: 1;
+}
+
+.font-size-text {
+  font-size: 26rpx;
+  color: #667eea;
+  min-width: 90rpx;
+  text-align: right;
+  font-weight: 600;
+  background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
+  padding: 8rpx 16rpx;
+  border-radius: 12rpx;
+}
+
+.color-picker {
+  display: flex;
+  gap: 24rpx;
+  justify-content: center;
+  padding: 10rpx 0;
+}
+
+.color-item {
+  width: 70rpx;
+  height: 70rpx;
+  border-radius: 50%;
+  border: 4rpx solid transparent;
+  transition: all 0.3s ease;
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
+  position: relative;
+}
+
+.color-item::after {
+  content: '';
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  width: 24rpx;
+  height: 24rpx;
+  background: #fff;
+  border-radius: 50%;
+  opacity: 0;
+  transition: opacity 0.3s ease;
+}
+
+.color-item.active {
+  border-color: #667eea;
+  transform: scale(1.1);
+  box-shadow: 0 6rpx 20rpx rgba(102, 126, 234, 0.3);
+}
+
+.color-item.active::after {
+  opacity: 1;
+}
+
+.mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  z-index: 99;
+  animation: fadeIn 0.3s ease;
+}
+
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}

+ 115 - 0
pages/register/register.js

@@ -0,0 +1,115 @@
+// pages/register/register.js
+const userApi = require('../../api/user');
+
+Page({
+  data: {
+    username: '',
+    password: '',
+    confirmPassword: '',
+    nickname: '',
+    phone: '',
+    email: '',
+    loading: false
+  },
+
+  // 输入用户名
+  onUsernameInput(e) {
+    this.setData({
+      username: e.detail.value
+    });
+  },
+
+  // 输入密码
+  onPasswordInput(e) {
+    this.setData({
+      password: e.detail.value
+    });
+  },
+
+  // 输入确认密码
+  onConfirmPasswordInput(e) {
+    this.setData({
+      confirmPassword: e.detail.value
+    });
+  },
+
+  // 输入昵称
+  onNicknameInput(e) {
+    this.setData({
+      nickname: e.detail.value
+    });
+  },
+
+  // 输入手机号
+  onPhoneInput(e) {
+    this.setData({
+      phone: e.detail.value
+    });
+  },
+
+  // 输入邮箱
+  onEmailInput(e) {
+    this.setData({
+      email: e.detail.value
+    });
+  },
+
+  // 注册
+  async handleRegister() {
+    const { username, password, confirmPassword, nickname, phone, email } = this.data;
+    
+    if (!username || !password) {
+      wx.showToast({
+        title: '请输入用户名和密码',
+        icon: 'none'
+      });
+      return;
+    }
+
+    if (password !== confirmPassword) {
+      wx.showToast({
+        title: '两次密码输入不一致',
+        icon: 'none'
+      });
+      return;
+    }
+
+    if (password.length < 6) {
+      wx.showToast({
+        title: '密码长度至少6位',
+        icon: 'none'
+      });
+      return;
+    }
+
+    this.setData({ loading: true });
+
+    try {
+      const userData = {
+        username,
+        password,
+        nickname: nickname || username,
+        phone: phone || '',
+        email: email || ''
+      };
+      
+      await userApi.userRegister(userData);
+      
+      wx.showToast({
+        title: '注册成功',
+        icon: 'success'
+      });
+
+      setTimeout(() => {
+        wx.navigateBack();
+      }, 1500);
+    } catch (error) {
+      wx.showToast({
+        title: error || '注册失败',
+        icon: 'none'
+      });
+    } finally {
+      this.setData({ loading: false });
+    }
+  }
+});

+ 3 - 0
pages/register/register.json

@@ -0,0 +1,3 @@
+{
+  "navigationBarTitleText": "注册"
+}

+ 77 - 0
pages/register/register.wxml

@@ -0,0 +1,77 @@
+<!--pages/register/register.wxml-->
+<view class="container">
+  <view class="register-box">
+    <view class="title">用户注册</view>
+    
+    <view class="form">
+      <view class="input-group">
+        <input 
+          class="input" 
+          type="text" 
+          placeholder="用户名(必填)"
+          value="{{username}}"
+          bindinput="onUsernameInput"
+        />
+      </view>
+      
+      <view class="input-group">
+        <input 
+          class="input" 
+          type="text" 
+          placeholder="昵称"
+          value="{{nickname}}"
+          bindinput="onNicknameInput"
+        />
+      </view>
+      
+      <view class="input-group">
+        <input 
+          class="input" 
+          type="password" 
+          placeholder="密码(必填,至少6位)"
+          value="{{password}}"
+          bindinput="onPasswordInput"
+        />
+      </view>
+      
+      <view class="input-group">
+        <input 
+          class="input" 
+          type="password" 
+          placeholder="确认密码"
+          value="{{confirmPassword}}"
+          bindinput="onConfirmPasswordInput"
+        />
+      </view>
+      
+      <view class="input-group">
+        <input 
+          class="input" 
+          type="number" 
+          placeholder="手机号(选填)"
+          value="{{phone}}"
+          bindinput="onPhoneInput"
+        />
+      </view>
+      
+      <view class="input-group">
+        <input 
+          class="input" 
+          type="text" 
+          placeholder="邮箱(选填)"
+          value="{{email}}"
+          bindinput="onEmailInput"
+        />
+      </view>
+      
+      <button 
+        class="register-btn" 
+        bindtap="handleRegister"
+        loading="{{loading}}"
+        disabled="{{loading}}"
+      >
+        注册
+      </button>
+    </view>
+  </view>
+</view>

+ 68 - 0
pages/register/register.wxss

@@ -0,0 +1,68 @@
+/* pages/register/register.wxss */
+.container {
+  min-height: 100vh;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 40rpx;
+}
+
+.register-box {
+  width: 100%;
+  max-width: 600rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 60rpx 40rpx;
+  box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.1);
+}
+
+.title {
+  font-size: 48rpx;
+  font-weight: bold;
+  color: #333;
+  text-align: center;
+  margin-bottom: 60rpx;
+}
+
+.form {
+  width: 100%;
+}
+
+.input-group {
+  margin-bottom: 30rpx;
+}
+
+.input {
+  width: 100%;
+  height: 88rpx;
+  padding: 0 30rpx;
+  border: 2rpx solid #e0e0e0;
+  border-radius: 10rpx;
+  font-size: 28rpx;
+  box-sizing: border-box;
+}
+
+.input:focus {
+  border-color: #667eea;
+}
+
+.register-btn {
+  width: 100%;
+  height: 88rpx;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  border-radius: 10rpx;
+  font-size: 32rpx;
+  font-weight: bold;
+  margin-top: 40rpx;
+  border: none;
+}
+
+.register-btn::after {
+  border: none;
+}
+
+.register-btn[disabled] {
+  opacity: 0.6;
+}

+ 85 - 0
pages/settings/settings.js

@@ -0,0 +1,85 @@
+// pages/settings/settings.js
+const userUtil = require('../../utils/user');
+const userApi = require('../../api/user');
+
+Page({
+  data: {
+    userInfo: null,
+    saving: false
+  },
+
+  onLoad() {
+    this.loadUserInfo();
+  },
+
+  // 加载用户信息
+  loadUserInfo() {
+    const userInfo = userUtil.getUserInfo();
+    this.setData({ userInfo });
+  },
+
+  // 昵称输入
+  onNicknameInput(e) {
+    this.setData({
+      'userInfo.nickname': e.detail.value
+    });
+  },
+
+  // 头像URL输入
+  onAvatarUrlInput(e) {
+    this.setData({
+      'userInfo.avatarUrl': e.detail.value
+    });
+  },
+
+  // 保存用户信息
+  async saveUserInfo() {
+    if (this.data.saving) return;
+
+    const { userInfo } = this.data;
+    if (!userInfo || !userInfo.userId) {
+      wx.showToast({
+        title: '用户信息不存在',
+        icon: 'none'
+      });
+      return;
+    }
+
+    this.setData({ saving: true });
+
+    try {
+      const updateData = {
+        nickname: userInfo.nickname || '',
+        avatarUrl: userInfo.avatarUrl || ''
+      };
+
+      await userApi.updateCurrentUserInfo(updateData);
+      
+      // 更新本地存储的用户信息
+      const token = wx.getStorageSync('token');
+      const updatedUserInfo = {
+        ...userInfo,
+        ...updateData
+      };
+      userUtil.saveUserInfo(updatedUserInfo, token);
+
+      wx.showToast({
+        title: '保存成功',
+        icon: 'success'
+      });
+
+      // 延迟返回上一页
+      setTimeout(() => {
+        wx.navigateBack();
+      }, 1500);
+    } catch (error) {
+      wx.showToast({
+        title: error || '保存失败',
+        icon: 'none'
+      });
+    } finally {
+      this.setData({ saving: false });
+    }
+  }
+});
+

+ 9 - 0
pages/settings/settings.json

@@ -0,0 +1,9 @@
+{
+  "navigationBarTitleText": "设置",
+  "navigationBarBackgroundColor": "#667eea",
+  "navigationBarTextStyle": "white",
+  "backgroundColor": "#f5f5f5"
+}
+
+
+

+ 50 - 0
pages/settings/settings.wxml

@@ -0,0 +1,50 @@
+<!--pages/settings/settings.wxml-->
+<view class="container">
+  <view class="header">
+    <text class="title">设置</text>
+  </view>
+
+  <view class="form-section">
+    <view class="form-item">
+      <text class="label">用户名</text>
+      <input 
+        class="input" 
+        type="text" 
+        value="{{userInfo.username}}" 
+        disabled
+        placeholder="用户名不可修改"
+      />
+      <text class="hint">用户名不可修改</text>
+    </view>
+
+    <view class="form-item">
+      <text class="label">昵称</text>
+      <input 
+        class="input" 
+        type="text" 
+        value="{{userInfo.nickname || ''}}" 
+        placeholder="请输入昵称"
+        bindinput="onNicknameInput"
+        maxlength="20"
+      />
+    </view>
+
+    <view class="form-item">
+      <text class="label">头像URL</text>
+      <input 
+        class="input" 
+        type="text" 
+        value="{{userInfo.avatarUrl || ''}}" 
+        placeholder="请输入头像URL"
+        bindinput="onAvatarUrlInput"
+      />
+    </view>
+  </view>
+
+  <view class="button-section">
+    <button class="save-btn" bindtap="saveUserInfo" loading="{{saving}}">保存</button>
+  </view>
+</view>
+
+
+

+ 114 - 0
pages/settings/settings.wxss

@@ -0,0 +1,114 @@
+/* pages/settings/settings.wxss */
+page {
+  padding: 0;
+  margin: 0;
+}
+
+.container {
+  min-height: 100vh;
+  background: linear-gradient(180deg, #f8f9ff 0%, #f5f5f5 100%);
+  padding: 0;
+  margin: 0;
+}
+
+.header {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  padding: 30rpx 20rpx 30rpx;
+  padding-top: calc(30rpx + env(safe-area-inset-top));
+  color: #fff;
+}
+
+.title {
+  font-size: 36rpx;
+  font-weight: 600;
+  text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
+}
+
+.form-section {
+  background: #fff;
+  margin: 0;
+  border-radius: 0;
+  overflow: hidden;
+  box-shadow: none;
+  width: 100%;
+}
+
+.form-item {
+  padding: 24rpx 20rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.form-item:last-child {
+  border-bottom: none;
+}
+
+.label {
+  display: block;
+  font-size: 26rpx;
+  color: #333;
+  font-weight: 500;
+  margin-bottom: 16rpx;
+}
+
+.input {
+  width: 100%;
+  height: 72rpx;
+  padding: 0 20rpx;
+  background: #f5f5f5;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+  color: #333;
+  border: 2rpx solid transparent;
+  transition: all 0.3s ease;
+  box-sizing: border-box;
+}
+
+.input:focus {
+  background: #fff;
+  border-color: #667eea;
+  box-shadow: 0 0 0 4rpx rgba(102, 126, 234, 0.1);
+}
+
+.input[disabled] {
+  background: #f0f0f0;
+  color: #999;
+}
+
+.hint {
+  display: block;
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 12rpx;
+}
+
+.button-section {
+  padding: 30rpx 20rpx 40rpx;
+}
+
+.save-btn {
+  width: 100%;
+  height: 88rpx;
+  line-height: 88rpx;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  border-radius: 16rpx;
+  font-size: 30rpx;
+  font-weight: 500;
+  border: none;
+  box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.3);
+  transition: all 0.3s ease;
+}
+
+.save-btn::after {
+  border: none;
+}
+
+.save-btn:active {
+  transform: scale(0.98);
+  box-shadow: 0 2rpx 12rpx rgba(102, 126, 234, 0.4);
+}
+
+.save-btn[loading] {
+  opacity: 0.7;
+}
+

+ 41 - 0
project.config.json

@@ -0,0 +1,41 @@
+{
+  "compileType": "miniprogram",
+  "libVersion": "3.11.0",
+  "packOptions": {
+    "ignore": [],
+    "include": []
+  },
+  "setting": {
+    "coverView": true,
+    "es6": true,
+    "postcss": true,
+    "minified": true,
+    "enhance": true,
+    "showShadowRootInWxmlPanel": true,
+    "packNpmRelationList": [],
+    "babelSetting": {
+      "ignore": [],
+      "disablePlugins": [],
+      "outputPath": ""
+    },
+    "compileWorklet": false,
+    "uglifyFileName": false,
+    "uploadWithSourceMap": true,
+    "packNpmManually": false,
+    "minifyWXSS": true,
+    "minifyWXML": true,
+    "localPlugins": false,
+    "condition": false,
+    "swc": false,
+    "disableSWC": true,
+    "disableUseStrict": false,
+    "useCompilerPlugins": false
+  },
+  "condition": {},
+  "editorSetting": {
+    "tabIndent": "auto",
+    "tabSize": 2
+  },
+  "appid": "wx295f1420623fed23",
+  "simulatorPluginLibVersion": {}
+}

+ 23 - 0
project.private.config.json

@@ -0,0 +1,23 @@
+{
+  "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
+  "projectname": "xiao",
+  "setting": {
+    "compileHotReLoad": true,
+    "urlCheck": false,
+    "coverView": true,
+    "lazyloadPlaceholderEnable": false,
+    "skylineRenderEnable": false,
+    "preloadBackgroundData": false,
+    "autoAudits": false,
+    "useApiHook": true,
+    "showShadowRootInWxmlPanel": true,
+    "useStaticServer": false,
+    "useLanDebug": false,
+    "showES6CompileOption": false,
+    "bigPackageSizeSupport": false,
+    "checkInvalidKey": true,
+    "ignoreDevUnusedFiles": true
+  },
+  "libVersion": "3.11.0",
+  "condition": {}
+}

+ 7 - 0
sitemap.json

@@ -0,0 +1,7 @@
+{
+  "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
+  "rules": [{
+  "action": "allow",
+  "page": "*"
+  }]
+}

+ 125 - 0
utils/api.js

@@ -0,0 +1,125 @@
+// API工具类
+// 开发环境:使用 localhost
+// 生产环境:需要配置为 HTTPS 域名
+const BASE_URL = 'http://localhost:8080/api';
+
+// 开发环境提示
+if (BASE_URL.includes('localhost')) {
+  console.warn('当前使用 localhost,请在微信开发者工具中关闭域名校验:详情 -> 本地设置 -> 不校验合法域名');
+}
+
+// 请求封装
+function request(url, method = 'GET', data = {}, needAuth = true) {
+  return new Promise((resolve, reject) => {
+    // 获取token
+    const token = wx.getStorageSync('token') || '';
+    
+    // 构建请求头
+    const header = {
+      'Content-Type': 'application/json'
+    };
+    
+    if (needAuth && token) {
+      header['Authorization'] = `Bearer ${token}`;
+    }
+    
+    wx.request({
+      url: BASE_URL + url,
+      method: method,
+      data: data,
+      header: header,
+      success: (res) => {
+        console.log('API请求成功:', {
+          url: BASE_URL + url,
+          statusCode: res.statusCode,
+          data: res.data
+        });
+        
+        if (res.statusCode === 200) {
+          if (res.data.code === 200) {
+            resolve(res.data.data);
+          } else {
+            console.error('API返回错误:', {
+              code: res.data.code,
+              message: res.data.message || res.data.msg
+            });
+            // token过期或未授权
+            if (res.data.code === 401 || res.statusCode === 401) {
+              wx.removeStorageSync('token');
+              wx.removeStorageSync('userInfo');
+              wx.reLaunch({
+                url: '/pages/login/login'
+              });
+            }
+            // 403禁止访问(如管理员尝试登录小程序)
+            if (res.data.code === 403 || res.statusCode === 403) {
+              // 不跳转,只显示错误信息
+            }
+            reject(res.data.message || res.data.msg || '请求失败');
+          }
+        } else if (res.statusCode === 400) {
+          // 400错误通常是参数问题
+          console.error('请求参数错误:', res.data);
+          reject(res.data.message || res.data.msg || '请求参数错误');
+        } else if (res.statusCode === 403) {
+          // 403禁止访问
+          console.error('访问被禁止:', res.data);
+          reject(res.data.message || res.data.msg || '访问被禁止');
+        } else {
+          console.error('HTTP错误:', res.statusCode, res.data);
+          reject('网络错误: ' + res.statusCode + (res.data ? (' - ' + (res.data.message || JSON.stringify(res.data))) : ''));
+        }
+      },
+      fail: (err) => {
+        console.error('API请求失败:', {
+          url: BASE_URL + url,
+          error: err
+        });
+        reject(err.errMsg || '网络请求失败');
+      }
+    });
+  });
+}
+
+// GET请求
+function get(url, data = {}, needAuth = true) {
+  return request(url, 'GET', data, needAuth);
+}
+
+// POST请求
+function post(url, data = {}, needAuth = true, params = {}) {
+  // 如果有查询参数,拼接到URL
+  if (Object.keys(params).length > 0) {
+    const queryString = Object.keys(params)
+      .map(key => `${key}=${encodeURIComponent(params[key])}`)
+      .join('&');
+    url = url + (url.includes('?') ? '&' : '?') + queryString;
+  }
+  return request(url, 'POST', data, needAuth);
+}
+
+// PUT请求
+function put(url, data = {}, needAuth = true) {
+  return request(url, 'PUT', data, needAuth);
+}
+
+// DELETE请求
+function del(url, data = {}, needAuth = true) {
+  // DELETE请求的参数通常放在URL中
+  if (Object.keys(data).length > 0) {
+    const queryString = Object.keys(data)
+      .map(key => `${key}=${encodeURIComponent(data[key])}`)
+      .join('&');
+    url = url + (url.includes('?') ? '&' : '?') + queryString;
+  }
+  return request(url, 'DELETE', {}, needAuth);
+}
+
+module.exports = {
+  request,
+  get,
+  post,
+  put,
+  del,
+  BASE_URL
+};

+ 47 - 0
utils/user.js

@@ -0,0 +1,47 @@
+// 用户相关工具函数
+
+// 检查是否登录
+function isLogin() {
+  const token = wx.getStorageSync('token');
+  return !!token;
+}
+
+// 获取用户信息
+function getUserInfo() {
+  return wx.getStorageSync('userInfo') || null;
+}
+
+// 保存用户信息
+function saveUserInfo(userInfo, token) {
+  wx.setStorageSync('userInfo', userInfo);
+  wx.setStorageSync('token', token);
+}
+
+// 清除用户信息
+function clearUserInfo() {
+  const userInfo = getUserInfo();
+  // 清除用户信息
+  wx.removeStorageSync('userInfo');
+  wx.removeStorageSync('token');
+  
+  // 清除该用户的聊天会话数据
+  if (userInfo && userInfo.userId) {
+    const chatSessionKey = `chat_sessionId_${userInfo.userId}`;
+    wx.removeStorageSync(chatSessionKey);
+    console.log('已清除用户聊天会话数据:', chatSessionKey);
+  }
+}
+
+// 检查是否为管理员
+function isAdmin() {
+  const userInfo = getUserInfo();
+  return userInfo && userInfo.userRole === 1;
+}
+
+module.exports = {
+  isLogin,
+  getUserInfo,
+  saveUserInfo,
+  clearUserInfo,
+  isAdmin
+};

+ 19 - 0
utils/util.js

@@ -0,0 +1,19 @@
+const formatTime = date => {
+  const year = date.getFullYear()
+  const month = date.getMonth() + 1
+  const day = date.getDate()
+  const hour = date.getHours()
+  const minute = date.getMinutes()
+  const second = date.getSeconds()
+
+  return `${[year, month, day].map(formatNumber).join('/')} ${[hour, minute, second].map(formatNumber).join(':')}`
+}
+
+const formatNumber = n => {
+  n = n.toString()
+  return n[1] ? n : `0${n}`
+}
+
+module.exports = {
+  formatTime
+}

+ 72 - 0
书籍封面说明.md

@@ -0,0 +1,72 @@
+# 书籍封面图片说明
+
+## 当前实现
+
+目前使用本地占位图来显示书籍封面。如果数据库中的 `coverUrl` 为空或加载失败,会显示一个渐变背景的占位图,上面显示书籍标题。
+
+## 如何添加本地封面图片
+
+### 方法1:使用本地图片文件(推荐)
+
+1. **准备图片**:
+   - 将书籍封面图片放入 `xiao/images/` 目录
+   - 建议尺寸:200x280 像素或等比例
+   - 格式:PNG 或 JPG
+
+2. **修改代码**:
+   在 `pages/detail/detail.wxml` 中修改:
+   ```xml
+   <image 
+     class="cover" 
+     src="{{content.coverUrl || '/images/books/default-cover.png'}}" 
+     mode="aspectFill" 
+   ></image>
+   ```
+
+3. **在数据库中设置封面URL**:
+   - 如果封面在 `images/books/` 目录下,数据库中的 `coverUrl` 设置为 `/images/books/封面名.png`
+   - 例如:`/images/books/huozhe.png`
+
+### 方法2:使用网络图片URL
+
+1. **上传图片到服务器**:
+   - 将封面图片上传到你的服务器或CDN
+   - 获取图片的完整URL
+
+2. **在数据库中设置**:
+   - 在 `content` 表的 `cover_url` 字段中填入完整的图片URL
+   - 例如:`https://your-domain.com/images/covers/huozhe.jpg`
+
+### 方法3:使用base64编码(小图片)
+
+1. **转换图片为base64**:
+   - 使用在线工具将图片转换为base64编码
+   - 例如:https://www.base64-image.de/
+
+2. **在代码中使用**:
+   ```xml
+   <image 
+     class="cover" 
+     src="{{content.coverUrl || 'data:image/png;base64,iVBORw0KGgoAAAANS...'}}" 
+     mode="aspectFill" 
+   ></image>
+   ```
+
+## 推荐方案
+
+**最佳实践**:
+1. 在服务器上创建一个图片存储目录(如 `/uploads/covers/`)
+2. 将书籍封面上传到该目录
+3. 在数据库的 `cover_url` 字段中存储相对路径或完整URL
+4. 小程序直接使用数据库中的URL显示
+
+## 当前占位图
+
+目前使用本地占位图(渐变背景 + 书籍标题文字),如果数据库中的 `coverUrl` 为空或图片加载失败,会自动显示占位图。占位图使用紫色渐变背景,白色文字显示书籍标题。
+
+## 注意事项
+
+1. **图片大小**:建议单个封面图片不超过 200KB
+2. **图片格式**:推荐使用 JPG(文件小)或 PNG(支持透明)
+3. **图片尺寸**:建议 200x280 像素(或等比例)
+4. **网络图片**:需要配置合法域名(生产环境)

+ 33 - 0
关于基础库提示.md

@@ -0,0 +1,33 @@
+# 关于"灰度中的基础库 3.11.0"提示
+
+## 这是什么意思?
+
+这个提示表示微信开发者工具正在使用一个**测试版本**的基础库(版本 3.11.0)。这不是错误,只是一个**信息提示**。
+
+## 是否有影响?
+
+- **一般不影响开发**:测试版本通常可以正常使用
+- **可能有一些新功能**:测试版本可能包含尚未正式发布的新功能
+- **可能有一些 bug**:因为是测试版本,可能会有一些不稳定的地方
+
+## 如何处理?
+
+### 方案1:忽略(推荐)
+
+如果小程序运行正常,可以**直接忽略**这个提示,继续开发。
+
+### 方案2:切换到稳定版本
+
+如果想使用稳定版本,可以:
+
+1. 在微信开发者工具中,点击右上角的 **"详情"** 按钮
+2. 在 **"本地设置"** 选项卡中
+3. 找到 **"基础库版本"** 设置
+4. 选择一个稳定版本(如 3.0.0、2.30.0 等)
+5. 重新编译项目
+
+## 建议
+
+- **开发阶段**:可以使用测试版本,体验新功能
+- **上线前**:建议切换到稳定版本进行测试
+- **目前**:如果小程序运行正常,可以暂时忽略这个提示

+ 25 - 0
创建占位图标说明.md

@@ -0,0 +1,25 @@
+# 占位图标创建说明
+
+由于无法直接创建图片文件,请按以下步骤手动创建占位图标:
+
+## 方法1:使用微信开发者工具(最简单)
+
+1. 在微信开发者工具中,右键点击 `images` 文件夹
+2. 选择"新建文件"
+3. 创建以下6个文件(可以是空文件,或者用任何图片工具创建81x81的PNG):
+   - `home.png`
+   - `home-active.png`
+   - `bookshelf.png`
+   - `bookshelf-active.png`
+   - `profile.png`
+   - `profile-active.png`
+
+## 方法2:使用在线工具快速生成
+
+访问 https://www.iconfont.cn/ 搜索并下载图标,或者使用任何图片编辑器创建81x81的PNG图片。
+
+## 方法3:暂时使用文字导航(当前方案)
+
+目前已经移除了 tabBar,使用自定义底部导航栏(emoji图标),所以暂时不需要图片文件。
+
+如果需要恢复 tabBar,请先创建上述图标文件。

+ 78 - 0
图标说明.md

@@ -0,0 +1,78 @@
+# TabBar 图标配置说明
+
+## 当前状态
+
+为了快速启动小程序,目前 tabBar 配置中暂时移除了图标路径,只保留了文字标签。小程序可以正常运行,但底部导航栏只显示文字。
+
+## 如何添加图标
+
+### 方案1:使用图标字体(推荐,简单快速)
+
+1. 在微信开发者工具中,tabBar 可以不配置图标,只显示文字即可正常使用。
+
+2. 如果需要图标,可以:
+   - 使用小程序内置的 icon 组件
+   - 或者使用 iconfont 图标字体
+   - 或者使用网络图标(需要配置合法域名)
+
+### 方案2:添加本地图标文件
+
+如果需要使用本地图标文件,需要:
+
+1. 在项目根目录创建 `images` 文件夹
+2. 准备以下图标文件(建议尺寸:81px × 81px):
+   - `home.png` - 首页未选中图标
+   - `home-active.png` - 首页选中图标
+   - `bookshelf.png` - 书架未选中图标
+   - `bookshelf-active.png` - 书架选中图标
+   - `profile.png` - 我的未选中图标
+   - `profile-active.png` - 我的选中图标
+
+3. 然后在 `app.json` 中恢复图标路径配置:
+
+```json
+"tabBar": {
+  "color": "#7A7E83",
+  "selectedColor": "#667eea",
+  "borderStyle": "black",
+  "backgroundColor": "#ffffff",
+  "list": [
+    {
+      "pagePath": "pages/index/index",
+      "iconPath": "images/home.png",
+      "selectedIconPath": "images/home-active.png",
+      "text": "首页"
+    },
+    {
+      "pagePath": "pages/bookshelf/bookshelf",
+      "iconPath": "images/bookshelf.png",
+      "selectedIconPath": "images/bookshelf-active.png",
+      "text": "书架"
+    },
+    {
+      "pagePath": "pages/profile/profile",
+      "iconPath": "images/profile.png",
+      "selectedIconPath": "images/profile-active.png",
+      "text": "我的"
+    }
+  ]
+}
+```
+
+## 图标资源推荐
+
+可以免费获取图标的地方:
+1. **IconFont** (https://www.iconfont.cn/) - 阿里巴巴图标库
+2. **Iconfinder** (https://www.iconfinder.com/) - 图标搜索
+3. **Flaticon** (https://www.flaticon.com/) - 免费图标库
+
+## 图标要求
+
+- 尺寸:建议 81px × 81px(小程序会自动缩放)
+- 格式:PNG 格式,支持透明背景
+- 大小:建议单个图标文件不超过 40KB
+- 颜色:未选中状态使用灰色,选中状态使用主题色(#667eea)
+
+## 当前配置
+
+目前 tabBar 配置为仅文字模式,功能完全正常,只是没有图标。如果需要添加图标,按照上述方案操作即可。

+ 103 - 0
添加TabBar图标指南.md

@@ -0,0 +1,103 @@
+# TabBar 图标添加指南
+
+## 当前状态
+
+`app.json` 中已经配置了图标路径,但需要确保图标文件存在。
+
+## 快速添加图标的方法
+
+### 方法1:使用在线图标生成器(推荐)
+
+1. **访问图标库网站**:
+   - IconFont: https://www.iconfont.cn/
+   - Iconfinder: https://www.iconfinder.com/
+   - Flaticon: https://www.flaticon.com/
+
+2. **搜索并下载图标**:
+   - 首页:搜索 "home" 或 "首页"
+   - 书架:搜索 "bookshelf" 或 "书架"
+   - 我的:搜索 "user" 或 "profile" 或 "我的"
+
+3. **准备图标文件**(需要6个文件):
+   - `images/home.png` - 首页未选中(灰色,81x81px)
+   - `images/home-active.png` - 首页选中(主题色 #667eea,81x81px)
+   - `images/bookshelf.png` - 书架未选中(灰色,81x81px)
+   - `images/bookshelf-active.png` - 书架选中(主题色 #667eea,81x81px)
+   - `images/profile.png` - 我的未选中(灰色,81x81px)
+   - `images/profile-active.png` - 我的选中(主题色 #667eea,81x81px)
+
+### 方法2:使用微信开发者工具创建占位图标
+
+1. 在微信开发者工具中,右键点击 `images` 文件夹
+2. 选择"新建文件"
+3. 创建6个PNG文件(可以是简单的占位图)
+
+### 方法3:使用图片编辑工具
+
+使用 Photoshop、GIMP 或在线编辑器创建 81x81px 的 PNG 图标。
+
+## 图标要求
+
+- **尺寸**:81px × 81px(推荐)
+- **格式**:PNG(支持透明背景)
+- **大小**:单个文件不超过 40KB
+- **颜色**:
+  - 未选中:灰色 (#7A7E83)
+  - 选中:主题色 (#667eea)
+
+## 配置检查
+
+确保 `app.json` 中的配置如下:
+
+```json
+"tabBar": {
+  "color": "#7A7E83",
+  "selectedColor": "#667eea",
+  "borderStyle": "black",
+  "backgroundColor": "#ffffff",
+  "list": [
+    {
+      "pagePath": "pages/index/index",
+      "iconPath": "images/home.png",
+      "selectedIconPath": "images/home-active.png",
+      "text": "首页"
+    },
+    {
+      "pagePath": "pages/bookshelf/bookshelf",
+      "iconPath": "images/bookshelf.png",
+      "selectedIconPath": "images/bookshelf-active.png",
+      "text": "书架"
+    },
+    {
+      "pagePath": "pages/profile/profile",
+      "iconPath": "images/profile.png",
+      "selectedIconPath": "images/profile-active.png",
+      "text": "我的"
+    }
+  ]
+}
+```
+
+## 推荐图标资源
+
+1. **IconFont** (https://www.iconfont.cn/)
+   - 搜索:首页、书架、用户
+   - 免费下载,支持自定义颜色
+
+2. **Iconfinder** (https://www.iconfinder.com/)
+   - 搜索:home, bookshelf, user
+   - 有免费和付费图标
+
+3. **Flaticon** (https://www.flaticon.com/)
+   - 大量免费图标
+   - 支持 PNG 格式下载
+
+## 注意事项
+
+- 图标文件必须放在 `images` 目录下
+- 文件名必须与 `app.json` 中配置的路径完全一致
+- 如果图标不显示,检查文件路径和文件名是否正确
+- 建议使用透明背景的 PNG 图标
+
+
+

+ 28 - 0
解决登录问题.md

@@ -0,0 +1,28 @@
+# 解决登录问题 - 域名校验
+
+## 问题原因
+
+微信小程序默认不允许访问 `http://localhost:8080`,需要在开发环境中关闭域名校验。
+
+## 解决方案
+
+### 方法1:关闭域名校验(开发阶段推荐)
+
+1. 在微信开发者工具中,点击右上角的 **"详情"** 按钮
+2. 在 **"本地设置"** 选项卡中
+3. 勾选 **"不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书"**
+4. 重新编译项目
+
+### 方法2:配置合法域名(生产环境)
+
+如果要在真机上测试,需要:
+1. 登录微信公众平台(https://mp.weixin.qq.com)
+2. 进入"开发" -> "开发管理" -> "开发设置"
+3. 在"服务器域名"中添加你的后端域名(需要 HTTPS)
+4. 注意:本地开发时仍需要使用方法1
+
+## 当前配置
+
+目前 API 地址设置为:`http://localhost:8080/api`
+
+如果是开发环境,请使用方法1关闭域名校验。

+ 57 - 0
问题修复说明.md

@@ -0,0 +1,57 @@
+# 问题修复说明
+
+## 已修复的问题
+
+### 1. API 请求 400 错误 ✅
+**问题**:请求参数中包含字符串 "null",导致后端无法解析
+**修复**:
+- 修改了 `api/content.js`,不再传递 null 值
+- 修改了 `pages/index/index.js`,正确处理 contentType 参数
+- 改进了错误提示
+
+### 2. 底部导航栏重复显示 ✅
+**问题**:同时存在 tabBar 和自定义导航栏,导致重复显示
+**修复**:
+- 移除了所有页面的自定义底部导航栏
+- 使用 app.json 中配置的 tabBar(原生导航栏)
+
+### 3. 图片加载问题
+**问题**:`/images/default-avatar.png` 和 `/images/default-cover.png` 不存在
+**解决方案**:
+- 这些图片路径在代码中作为默认值使用
+- 如果图片不存在,小程序会显示占位符或空白
+- 可以后续添加这些图片,或者修改代码使用其他占位方案
+
+## 需要手动操作
+
+### 1. 重启后端服务
+修复代码后,需要重启 Spring Boot 后端服务才能生效。
+
+### 2. 重新编译小程序
+在微信开发者工具中点击"编译"按钮,重新运行小程序。
+
+### 3. 添加默认图片(可选)
+如果需要显示默认图片,可以在 `images` 目录下添加:
+- `default-cover.png` - 默认书籍封面
+- `default-avatar.png` - 默认用户头像
+
+或者修改代码中的图片路径,使用网络图片或 base64 编码的占位图。
+
+## 测试建议
+
+1. **首页加载**:检查是否能正常加载书籍列表
+2. **分类筛选**:测试切换分类是否正常
+3. **内容类型**:测试"全部"、"电子书"、"听书"切换
+4. **搜索功能**:测试搜索是否正常
+5. **底部导航**:检查是否只有一个导航栏,且可以正常切换
+
+## 如果还有问题
+
+1. 检查后端服务是否正常运行(http://localhost:8080)
+2. 检查是否关闭了域名校验(详情 -> 本地设置)
+3. 查看控制台错误信息,定位具体问题
+
+
+
+
+