浏览代码

新增搜索功能,优化书籍详情跳转逻辑,完善书架页面和分类页面的用户交互,修复样式问题,更新请求工具函数以支持登录状态管理。

yangwenlan 1 年之前
父节点
当前提交
9157d4e22c

+ 36 - 6
api/api.js

@@ -16,27 +16,35 @@ const addTokenToHeader = () => {
 }
 
 export const getBannerList = (channel) => {
-    return requestAll('/novel/banner', {channel:channel}, 'GET', addTokenToHeader())
+    return requestAll('/novel/banner', {channel}, 'GET', addTokenToHeader())
 }
 
 // /novel/cardNovels
 export const getCardNovels = (channel) => {
-    return requestAll('/novel/cardNovels', {channel:channel}, 'GET', addTokenToHeader())
+    return requestAll('/novel/cardNovels', {channel}, 'GET', addTokenToHeader())
 }
 
 // /novel/cardNovelsMore
 export const getCardNovelsMore = (id) => {
+    if (!id) {
+        return Promise.reject(new Error('缺少必要参数: id'));
+    }
     return requestAll('/novel/cardNovelsMore', {id}, 'GET', addTokenToHeader())
 }
 
 // /category/list
 export const getCategoryList = (category) => {
-    console.log('category',category)
+    if (!category) {
+        return Promise.reject(new Error('缺少必要参数: category'));
+    }
     return requestAll('/category/list', {category}, 'GET', addTokenToHeader())
 }
 
 // /novel/detail
 export const getNovelDetail = (id) => {
+    if (!id) {
+        return Promise.reject(new Error('缺少必要参数: id'));
+    }
     return requestAll('/novel/detail', {id}, 'GET', addTokenToHeader())
 }
 
@@ -45,8 +53,30 @@ export const getNovelSearch = ({
     keyword,
     page=1,
     size=10,
-    category,
-    isOver=0
+    category
 }) => {
-    return requestAll('/novel/search', {keyword,page,size,category,isOver}, 'GET', addTokenToHeader())
+    // 创建基本参数对象
+    const params = { keyword, page, size };
+    
+    // 如果category有值,才添加到参数中
+    if (category) {
+        params.category = category;
+    }
+    
+    return requestAll('/novel/search', params, 'GET', addTokenToHeader());
+}
+
+// /user/bookshelf 获取书架列表
+export const getBookshelfList = () => {
+    return requestAll('/user/bookshelf', {}, 'GET', addTokenToHeader())
+}
+
+// /user/bookshelf 添加书架
+export const addBookshelf = (data) => {
+    return requestAll('/user/bookshelf', data, 'POST', addTokenToHeader())
+}
+
+// /user/bookshelf 移除用户书架
+export const removeBookshelf = (data) => {
+    return requestAll('/user/bookshelf', data, 'DELETE', addTokenToHeader())
 }

+ 6 - 0
app.js

@@ -93,6 +93,11 @@ function onNovelPluginLoad(data) {
     ],
   })
 
+  function startRead() {
+    console.log('开始阅读');
+    //通报下后端
+  }
+
   // 监听用户行为事件
   novelManager.onUserTriggerEvent(res => {
     const { event_id } = res;
@@ -102,6 +107,7 @@ function onNovelPluginLoad(data) {
     switch(event_id) {
       case 'start_read': // 开始阅读
         console.log('开始阅读章节:', res.chapter_id);
+        startRead();
         break;
       
       case 'leave_readpage': // 离开阅读页

+ 2 - 1
app.json

@@ -6,7 +6,8 @@
     "pages/benefit/benefit",
     "pages/category/category",
     "pages/mine/mine",
-    "pages/book/list"
+    "pages/book/list",
+    "pages/search/search"
   ],
   "window": {
     "navigationStyle": "custom",

+ 5 - 2
pages/book/list.wxss

@@ -1,7 +1,9 @@
 /* pages/book/list.wxss */
 .container {
-  min-height: 100vh;
+  height: 100vh;
   padding: 80rpx 0;
+  box-sizing: border-box;
+  overflow: hidden;
 }
 
 /* 顶部标题栏 */
@@ -53,8 +55,9 @@
 
 /* 书籍列表 */
 .book-list {
-  height: calc(100vh - 90rpx);
+  height: calc(100vh - 260rpx);
   padding: 20rpx;
+  width: 720rpx;
 }
 
 .book-item {

+ 72 - 3
pages/bookshelf/bookshelf.js

@@ -1,8 +1,77 @@
+import { getBookshelfList } from '../../api/api'
+
 Page({
   data: {
-    
+    activeTab: 'history',
+    bookList: [],
+    loading: false,
+    hasMore: true,
+    page: 1,
+    size: 10
   },
-  onLoad: function(options) {
-    
+
+  onLoad() {
+    this.loadBookList()
+  },
+
+  onShow() {
+    // 每次显示页面时刷新列表
+    this.setData({
+      page: 1,
+      bookList: []
+    })
+    this.loadBookList()
+  },
+
+  // 切换标签
+  switchTab(e) {
+    const tab = e.currentTarget.dataset.tab
+    this.setData({ 
+      activeTab: tab,
+      page: 1,
+      bookList: []
+    })
+    this.loadBookList()
+  },
+
+  // 加载书架列表
+  async loadBookList() {
+    if (this.data.loading) return
+
+    this.setData({ loading: true })
+
+    try {
+      const result = await getBookshelfList()
+      
+      if (Array.isArray(result)) {
+        this.setData({
+          bookList: result,
+          loading: false,
+          hasMore: false // 目前接口不支持分页,所以直接设置为false
+        })
+      } else {
+        this.setData({
+          loading: false,
+          hasMore: false
+        })
+      }
+    } catch (error) {
+      console.error('加载书架失败:', error)
+      this.setData({ 
+        loading: false,
+        hasMore: false
+      })
+      wx.showToast({
+        title: '加载失败,请重试',
+        icon: 'none'
+      })
+    }
+  },
+
+  // 加载更多
+  loadMore() {
+    if (this.data.hasMore) {
+      this.loadBookList()
+    }
   }
 })

+ 37 - 5
pages/bookshelf/bookshelf.wxml

@@ -1,9 +1,41 @@
 <!--pages/bookshelf/bookshelf.wxml-->
 <view class="container">
-  <view class="header">
-    <text class="title">我的书架</text>
-  </view>
-  <view class="content">
-    <text>书架页面内容</text>
+  <!-- 顶部标签页 -->
+  <view class="tabs">
+    <view class="tab {{activeTab === 'history' ? 'active' : ''}}" bindtap="switchTab" data-tab="history">
+      阅读记录
+    </view>
+    <view class="tab {{activeTab === 'edit' ? 'active' : ''}}" bindtap="switchTab" data-tab="edit">
+      编辑书架
+    </view>
   </view>
+
+  <!-- 书籍列表 -->
+  <scroll-view scroll-y class="book-list" bindscrolltolower="loadMore">
+    <view class="book-grid">
+      <view class="book-item" wx:for="{{bookList}}" wx:key="id">
+        <view class="book-cover-wrap">
+          <image class="book-cover" src="{{item.novelCover}}" mode="aspectFill"/>
+          <text class="read-status">读过</text>
+        </view>
+        <text class="book-title">{{item.novelTitle}}</text>
+        <text class="latest-chapter">{{item.novelChapterTitle}}</text>
+      </view>
+    </view>
+    
+    <!-- 加载状态 -->
+    <view class="loading" wx:if="{{loading}}">
+      <text>加载中...</text>
+    </view>
+    
+    <!-- 没有更多 -->
+    <view class="no-more" wx:if="{{!loading && bookList.length > 0}}">
+      <text>没有更多了</text>
+    </view>
+    
+    <!-- 空状态 -->
+    <view class="empty" wx:if="{{!loading && bookList.length === 0}}">
+      <text>暂无阅读记录</text>
+    </view>
+  </scroll-view>
 </view>

+ 97 - 10
pages/bookshelf/bookshelf.wxss

@@ -1,21 +1,108 @@
 /* pages/bookshelf/bookshelf.wxss */
 .container {
-  padding: 30rpx;
+  min-height: 100vh;
+  background: #fff;
+  align-items: flex-start;
 }
 
-.header {
-  text-align: center;
-  margin-bottom: 30rpx;
+.tabs {
+  display: flex;
+  padding: 20rpx 40rpx;
+}
+
+.tab {
+  position: relative;
+  font-size: 32rpx;
+  color: #666;
+  margin-right: 60rpx;
+}
+
+.tab.active {
+  color: #333;
+  font-weight: 500;
+}
+
+.tab.active::after {
+  content: '';
+  position: absolute;
+  bottom: -20rpx;
+  left: 0;
+  width: 100%;
+  height: 6rpx;
+  background: linear-gradient(to right, #007aff, #af97ed);
+  border-radius: 3rpx;
+}
+
+.book-list {
+  height: calc(100vh - 100rpx);
+  padding: 20rpx;
 }
 
-.title {
-  font-size: 36rpx;
-  font-weight: bold;
+.book-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 30rpx;
+  padding: 10rpx;
 }
 
-.content {
+.book-item {
   display: flex;
-  justify-content: center;
+  flex-direction: column;
   align-items: center;
-  height: 800rpx;
+}
+
+.book-cover-wrap {
+  position: relative;
+  width: 200rpx;
+  height: 266rpx;
+  margin-bottom: 16rpx;
+}
+
+.book-cover {
+  width: 100%;
+  height: 100%;
+  border-radius: 12rpx;
+}
+
+.read-status {
+  position: absolute;
+  top: 10rpx;
+  left: 10rpx;
+  padding: 4rpx 12rpx;
+  background: rgba(0, 0, 0, 0.6);
+  color: #fff;
+  font-size: 20rpx;
+  border-radius: 8rpx;
+}
+
+.book-title {
+  width: 200rpx;
+  font-size: 28rpx;
+  color: #333;
+  text-align: center;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.latest-chapter {
+  width: 200rpx;
+  font-size: 24rpx;
+  color: #999;
+  text-align: center;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  margin-top: 8rpx;
+}
+
+.loading, .no-more, .empty {
+  padding: 30rpx 0;
+  text-align: center;
+  color: #999;
+  font-size: 28rpx;
+}
+
+.empty {
+  padding: 100rpx 0;
 }

+ 20 - 2
pages/category/category.js

@@ -1,4 +1,5 @@
 import { getCategoryList, getNovelSearch } from '../../api/api'
+import { goToBookDetail } from '../../utils/util'
 
 Page({
   data: {
@@ -100,8 +101,7 @@ Page({
         keyword: this.data.keyword,
         page: this.data.page,
         size: this.data.size,
-        category: category,
-        isOver: 0
+        category: category
       };
 
       const result = await getNovelSearch(params);
@@ -140,5 +140,23 @@ Page({
     if (this.data.currentCategory && this.data.hasMore) {
       this.loadBookList(this.data.currentCategory);
     }
+  },
+
+  // 跳转到书籍详情
+  handleBookTap: function(e) {
+    const { id, wxbookid } = e.currentTarget.dataset;
+    goToBookDetail({
+      bookId: id,
+      wxBookId: wxbookid
+    });
+  },
+
+  // 跳转到搜索页面
+  goToSearch: function(e) {
+    // 如果有输入内容,则带上关键词
+    const keyword = e.detail?.value || '';
+    wx.navigateTo({
+      url: `/pages/search/search?keyword=${encodeURIComponent(keyword)}`
+    });
   }
 })

+ 14 - 3
pages/category/category.wxml

@@ -3,9 +3,14 @@
   <!-- 顶部性别切换和搜索 -->
   <view class="header">
     <gender-tabs gender="{{gender}}" bind:switch="switchGender"/>
-    <view class="search-box">
+    <view class="search-box" bindtap="goToSearch">
       <icon type="search" size="14" color="#999"/>
-      <input type="text" placeholder="请输入书名/口令搜索" placeholder-class="placeholder"/>
+      <input 
+        type="text" 
+        placeholder="请输入书名/口令搜索" 
+        placeholder-class="placeholder"
+        disabled="true"
+      />
     </view>
   </view>
 
@@ -30,7 +35,13 @@
       bindscrolltolower="onScrollToLower"
       lower-threshold="50"
     >
-      <view class="book-item" wx:for="{{bookList}}" wx:key="id">
+      <view class="book-item" 
+        wx:for="{{bookList}}" 
+        wx:key="id"
+        bindtap="handleBookTap"
+        data-id="{{item.id}}"
+        data-wxbookid="{{item.wxBookId}}"
+      >
         <image class="book-cover" src="{{item.cover}}" mode="aspectFill"/>
         <view class="book-info">
           <text class="book-title">{{item.title}}</text>

+ 64 - 93
pages/index/index.js

@@ -2,12 +2,13 @@
 const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
 const testCover = '/assets/images/bg-book.png'
 import { getBannerList, getCardNovels } from '../../api/api'
+import { goToBookDetail } from '../../utils/util'
 
 const app = getApp()
 
 Page({
   data: {
-    isLoggingIn: true, // 添加登录状态
+    isLoggedIn: false,
     // 轮播配置
     indicatorDots: true,
     autoplay: true,
@@ -23,56 +24,14 @@ Page({
     recommendBooks: [], // 主编推荐数据
     
     // 推荐书籍列表
-    bookList: [
-      {
-        id: 1,
-        title: '你好可空',
-        cover: testCover
-      },
-      {
-        id: 2,
-        title: '盖世人屠',
-        cover: testCover
-      },
-      {
-        id: 3,
-        title: '倾听男女心声',
-        cover: testCover
-      },
-      {
-        id: 4,
-        title: '至尊圣医',
-        cover: testCover
-      }
-    ],
+    bookList: [],
     
     // 全网热推
     hotBooks: [], // 全网热推数据
     
     // 强力推荐
     showStrongRecommend: false, // 控制强力推荐模块显示/隐藏
-    strongBooks: [
-      {
-        id: 1,
-        title: '她重归家门虐前夫',
-        cover: testCover
-      },
-      {
-        id: 2,
-        title: '离婚后,我和总裁复合了',
-        cover: testCover
-      },
-      {
-        id: 3,
-        title: '娃儿,下山啦',
-        cover: testCover
-      },
-      {
-        id: 4,
-        title: '寒门千金',
-        cover: testCover
-      }
-    ],
+    strongBooks: [],
     
     motto: 'Hello World',
     userInfo: {
@@ -88,44 +47,10 @@ Page({
     this.autoLogin();
   },
 
-  // 自动登录
-  async autoLogin() {
-    if (app.checkLoginStatus()) {
-      this.setData({ isLoggingIn: false });
-      this.initPageData();
-      return;
-    }
-
-    try {
-      await app.login();
-      this.setData({ isLoggingIn: false });
-      this.initPageData();
-    } catch (error) {
-      console.error('登录失败:', error);
-      wx.showToast({
-        title: '登录失败,请重启小程序',
-        icon: 'none',
-        duration: 2000
-      });
-    }
-  },
-
-  // 初始化页面数据
-  initPageData() {
-    // 从本地存储读取性别设置
-    this.syncGenderState();
-    // 获取banner数据 channel 频道 1 男 2 女
-    this.getBannerList();
-    this.getCardNovels();
-  },
-
   onShow() {
-    if (!app.checkLoginStatus()) {
-      this.autoLogin();
-      return;
+    if (this.data.isLoggedIn) {
+      this.syncGenderState();
     }
-    // 每次页面显示时同步性别状态
-    this.syncGenderState();
   },
 
   onTabItemTap() {
@@ -133,15 +58,42 @@ Page({
     this.syncGenderState();
   },
 
+  // 自动登录
+  async autoLogin() {
+    try {
+      // 检查是否已经登录
+      const token = wx.getStorageSync('accessToken');
+      if (token) {
+        this.setData({ isLoggedIn: true });
+        this.syncGenderState();
+        return;
+      }
+
+      const res = await getApp().login();
+      this.setData({ isLoggedIn: true });
+      this.syncGenderState();
+    } catch (error) {
+      console.error('登录失败:', error);
+    }
+  },
+
   // 同步性别状态
-  syncGenderState() {
+  async syncGenderState() {
     const gender = wx.getStorageSync('gender') || 'male';
     if (gender !== this.data.gender) {
       this.setData({ gender });
-      // 重新加载数据
-      this.loadGenderData(gender);
+      await this.loadData();
     }
   },
+
+  // 加载数据
+  async loadData() {
+    // 从本地存储读取性别设置
+    this.syncGenderState();
+    // 获取banner数据 channel 频道 1 男 2 女
+    this.getBannerList();
+    this.getCardNovels();
+  },
   
   // 切换性别分类
   switchGender: function(e) {
@@ -150,9 +102,13 @@ Page({
     if (this.data.gender === gender) {
       return;
     }
+    
+    // 保存性别设置到本地存储
+    wx.setStorageSync('gender', gender);
+    
     this.setData({ gender });
-    // 根据性别加载不同的书籍数据
-    this.initPageData();
+    // 重新加载数据
+    this.loadData();
   },
   
   // 加载性别相关数据
@@ -170,15 +126,24 @@ Page({
   // 跳转到搜索页面
   goToSearch: function() {
     wx.navigateTo({
-      url: '/pages/search/search'
+      url: '/pages/search/search',
     });
   },
   
   // 跳转到书籍详情页
   goToBookDetail: function(e) {
-    const bookId = 'A1HcfuuvKqNdTuMEfF4DMKXo5A'; // 暂时写死的书籍ID
-    wx.navigateTo({
-      url: `plugin-private://wx293c4b6097a8a4d0/pages/novel/index?bookId=${bookId}`
+    const bookId = e.currentTarget.dataset.bookId;
+    const wxBookId = e.currentTarget.dataset.wxBookId;
+    const chapterId = e.currentTarget.dataset.chapterId;
+
+    console.log('bookId',bookId);
+    console.log('wxBookId',wxBookId);
+    console.log('chapterId',chapterId);
+    
+    goToBookDetail({
+      bookId,
+      wxBookId,
+      chapterId
     });
   },
   
@@ -282,12 +247,16 @@ Page({
             title: firstBook.title,
             desc: firstBook.brief,
             stats: `近期收藏${firstBook.readingNumber}`,
-            cover: firstBook.cover
+            cover: firstBook.cover,
+            wxBookId: firstBook.wxBookId,
+            chapterId: firstBook.chapterId
           }] : [],
           bookList: restBooks.slice(0, 4).map(novel => ({
             id: novel.id,
             title: novel.title,
-            cover: novel.cover
+            cover: novel.cover,
+            wxBookId: novel.wxBookId,
+            chapterId: novel.chapterId
           })),
           editorTitle: cardNovels[0].name // 设置实际的标题
         });
@@ -301,7 +270,9 @@ Page({
             title: novel.title,
             desc: novel.brief,
             stats: `${novel.readingNumber}人看过`,
-            cover: novel.cover
+            cover: novel.cover,
+            wxBookId: novel.wxBookId,
+            chapterId: novel.chapterId
           })),
           hotTitle: cardNovels[1].name // 设置实际的标题
         });

+ 27 - 5
pages/index/index.wxml

@@ -22,7 +22,8 @@
       <gender-tabs gender="{{gender}}" bind:switch="switchGender"/>
       <view class="search-box" bindtap="goToSearch">
         <icon type="search" size="14" color="#999"></icon>
-        <input placeholder="请输入书名/口令搜索" disabled="true" />
+        <input placeholder="请输入书名/口令搜索" disabled="true" placeholder-class="placeholder"/>
+        <view class="search-placeholder"></view>
       </view>
     </view>
 
@@ -66,7 +67,10 @@
     </view>
 
     <view class="recommend-main">
-      <view class="recommend-large" wx:if="{{recommendBooks[0]}}" bindtap="goToBookDetail" data-id="{{recommendBooks[0].id}}">
+      <view class="recommend-large" wx:if="{{recommendBooks[0]}}" bindtap="goToBookDetail" 
+        data-book-id="{{recommendBooks[0].id}}" 
+        data-wx-book-id="{{recommendBooks[0].wxBookId}}"
+        data-chapter-id="{{recommendBooks[0].chapterId}}">
         <image src="{{recommendBooks[0].cover}}" mode="aspectFill"></image>
         <view class="book-info">
           <view class="book-title">{{recommendBooks[0].title}}</view>
@@ -77,7 +81,13 @@
     </view>
 
     <view class="recommend-books">
-      <view class="book-item" wx:for="{{bookList}}" wx:key="id" bindtap="goToBookDetail" data-id="{{item.id}}">
+      <view class="book-item" 
+        wx:for="{{bookList}}" 
+        wx:key="id" 
+        bindtap="goToBookDetail" 
+        data-book-id="{{item.id}}"
+        data-wx-book-id="{{item.wxBookId}}"
+        data-chapter-id="{{item.chapterId}}">
         <image src="{{item.cover}}" mode="aspectFill"></image>
         <text>{{item.title}}</text>
       </view>
@@ -92,7 +102,13 @@
     </view>
 
     <view class="hot-books">
-      <view class="hot-book-item" wx:for="{{hotBooks}}" wx:key="id" bindtap="goToBookDetail" data-id="{{item.id}}">
+      <view class="hot-book-item" 
+        wx:for="{{hotBooks}}" 
+        wx:key="id" 
+        bindtap="goToBookDetail" 
+        data-book-id="{{item.id}}"
+        data-wx-book-id="{{item.wxBookId}}"
+        data-chapter-id="{{item.chapterId}}">
         <image src="{{item.cover}}" mode="aspectFill"></image>
         <view class="hot-book-info">
           <view class="hot-book-title">{{item.title}}</view>
@@ -111,7 +127,13 @@
     </view>
 
     <view class="strong-books">
-      <view class="strong-book-item" wx:for="{{strongBooks}}" wx:key="id" bindtap="goToBookDetail" data-id="{{item.id}}">
+      <view class="strong-book-item" 
+        wx:for="{{strongBooks}}" 
+        wx:key="id" 
+        bindtap="goToBookDetail" 
+        data-book-id="{{item.id}}"
+        data-wx-book-id="{{item.wxBookId}}"
+        data-chapter-id="{{item.chapterId}}">
         <image src="{{item.cover}}" mode="aspectFill"></image>
         <text>{{item.title}}</text>
       </view>

+ 156 - 0
pages/search/search.js

@@ -0,0 +1,156 @@
+import { getNovelSearch } from '../../api/api'
+import { goToBookDetail } from '../../utils/util'
+
+Page({
+  data: {
+    keyword: '',
+    bookList: [],
+    loading: false,
+    hasMore: true,
+    page: 1,
+    size: 10
+  },
+
+  onLoad: function(options) {
+    // 获取传入的关键词
+    const keyword = options.keyword || '';
+    this.setData({ keyword });
+    
+    // 如果有关键词,立即搜索
+    if (keyword) {
+      this.search();
+    }
+  },
+
+  // 输入框内容变化
+  handleInput: function(e) {
+    this.setData({
+      keyword: e.detail.value
+    });
+  },
+
+  // 清空搜索内容
+  clearSearch: function() {
+    this.setData({
+      keyword: '',
+      bookList: []
+    });
+  },
+
+  // 开始搜索
+  search: function() {
+    // 如果关键词为空,不执行搜索
+    if (!this.data.keyword.trim()) {
+      return;
+    }
+
+    // 重置搜索状态
+    this.setData({
+      page: 1,
+      bookList: [],
+      hasMore: true
+    });
+
+    // 执行搜索
+    this.loadSearchResult();
+  },
+
+  // 加载搜索结果
+  async loadSearchResult() {
+    if (this.data.loading || !this.data.hasMore) return;
+
+    this.setData({ loading: true });
+    
+    try {
+      const params = {
+        keyword: this.data.keyword,
+        page: this.data.page,
+        size: this.data.size
+      };
+
+      const result = await getNovelSearch(params);
+      
+      if (result && result.records) {
+        // 处理搜索结果,标记匹配关键字的部分
+        const processedList = this.processBookList(result.records);
+        
+        const newList = this.data.page === 1 ? processedList : [...this.data.bookList, ...processedList];
+        const hasMore = !result.last;
+        
+        this.setData({
+          bookList: newList,
+          loading: false,
+          hasMore: hasMore,
+          page: hasMore ? this.data.page + 1 : this.data.page
+        });
+      } else {
+        this.setData({
+          loading: false,
+          hasMore: false
+        });
+      }
+    } catch (error) {
+      console.error('搜索失败:', error);
+      this.setData({ 
+        loading: false,
+        hasMore: false
+      });
+      wx.showToast({
+        title: '搜索失败,请重试',
+        icon: 'none'
+      });
+    }
+  },
+
+  // 处理搜索结果,标记匹配关键字的部分
+  processBookList(books) {
+    if (!this.data.keyword || !books || !books.length) return books;
+    
+    const keyword = this.data.keyword.trim();
+    
+    return books.map(book => {
+      if (book.title && keyword) {
+        // 添加标记,表示这本书的标题包含关键字
+        book.titleContainsKeyword = book.title.toLowerCase().includes(keyword.toLowerCase());
+        
+        // 添加处理后的标题,用于在WXML中展示
+        // 由于小程序不支持直接用rich-text高亮,我们标记出匹配部分在后续WXML中用view嵌套处理
+        const titleLower = book.title.toLowerCase();
+        const keywordLower = keyword.toLowerCase();
+        
+        if (book.titleContainsKeyword) {
+          const index = titleLower.indexOf(keywordLower);
+          if (index !== -1) {
+            book.titleBeforeKeyword = book.title.substring(0, index);
+            book.titleKeyword = book.title.substring(index, index + keyword.length);
+            book.titleAfterKeyword = book.title.substring(index + keyword.length);
+          }
+        }
+      }
+      return book;
+    });
+  },
+
+  // 监听滚动到底部
+  onScrollToLower: function() {
+    if (this.data.hasMore) {
+      this.loadSearchResult();
+    }
+  },
+
+  // 跳转到书籍详情
+  handleBookTap: function(e) {
+    const { id, wxbookid } = e.currentTarget.dataset;
+    goToBookDetail({
+      bookId: id,
+      wxBookId: wxbookid
+    });
+  },
+
+  // 返回上一页
+  goBack: function() {
+    wx.navigateBack({
+      delta: 1
+    });
+  }
+}) 

+ 4 - 0
pages/search/search.json

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

+ 81 - 0
pages/search/search.wxml

@@ -0,0 +1,81 @@
+<!--pages/search/search.wxml-->
+<view class="container">
+  <!-- 顶部标题栏 -->
+  <view class="header">
+    <view class="back-btn" bindtap="goBack">
+      <view class="icon-back">
+        <view class="icon-back-arrow"></view>
+      </view>
+    </view>
+    <text class="title">搜索</text>
+  </view>
+
+  <!-- 搜索框 -->
+    <view class="search-box">
+      <icon type="search" size="20" color="#999"/>
+      <input 
+        type="text" 
+        placeholder="请输入书名/口令搜索" 
+        placeholder-class="placeholder"
+        value="{{keyword}}"
+        bindinput="handleInput"
+        bindconfirm="search"
+        confirm-type="search"
+        focus="true"
+      />
+      <icon wx:if="{{keyword}}" type="clear" size="20" color="#999" catchtap="clearSearch"/>
+      <view wx:if="{{keyword}}" bindtap="search" class="search-btn">搜索</view>
+    </view>
+    
+
+  <!-- 搜索结果 -->
+  <scroll-view 
+    scroll-y 
+    class="search-result"
+    bindscrolltolower="onScrollToLower"
+    lower-threshold="50"
+  >
+    <!-- 书籍列表 -->
+    <view class="book-list">
+      <view class="book-item" 
+        wx:for="{{bookList}}" 
+        wx:key="id"
+        bindtap="handleBookTap"
+        data-id="{{item.id}}"
+        data-wxbookid="{{item.wxBookId}}"
+      >
+        <image class="book-cover" src="{{item.cover}}" mode="aspectFill"/>
+        <view class="book-info">
+          <!-- 标题显示,带关键字高亮 -->
+          <view class="book-title">
+            <block wx:if="{{item.titleContainsKeyword}}">
+              <text>{{item.titleBeforeKeyword}}</text>
+              <text class="highlight">{{item.titleKeyword}}</text>
+              <text>{{item.titleAfterKeyword}}</text>
+            </block>
+            <block wx:else>
+              <text>{{item.title}}</text>
+            </block>
+          </view>
+          <text class="book-desc">{{item.brief}}</text>
+          <text class="book-author">{{item.author || '未知作者'}}</text>
+        </view>
+      </view>
+    </view>
+    
+    <!-- 加载状态 -->
+    <view class="loading" wx:if="{{loading}}">
+      <text>加载中...</text>
+    </view>
+    
+    <!-- 没有更多数据 -->
+    <view class="no-more" wx:if="{{!loading && !hasMore && bookList.length > 0}}">
+      <text>没有更多了</text>
+    </view>
+    
+    <!-- 搜索结果为空 -->
+    <view class="empty" wx:if="{{!loading && bookList.length === 0 && keyword}}">
+      <text>未找到相关结果</text>
+    </view>
+  </scroll-view>
+</view> 

+ 166 - 0
pages/search/search.wxss

@@ -0,0 +1,166 @@
+/* pages/search/search.wxss */
+.container {
+  min-height: 100vh;
+  background: #fff;
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box;
+  padding-top: 80rpx;
+}
+
+/* 顶部标题栏 */
+.header {
+  padding: 30rpx;
+  display: flex;
+  align-items: center;
+  z-index: 100;
+  border-bottom: 1rpx solid #eee;
+  width: 700rpx;
+}
+
+.back-btn {
+  width: 50rpx;
+  height: 50rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 30rpx;
+}
+
+/* 返回箭头图标 */
+.icon-back {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.icon-back-arrow {
+  width: 24rpx;
+  height: 24rpx;
+  border-top: 4rpx solid #333;
+  border-left: 4rpx solid #333;
+  transform: rotate(-45deg);
+  margin-left: 8rpx;
+}
+
+.title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+  flex: 1;
+  text-align: center;
+  margin-right: 80rpx; /* 为了保持标题居中,右侧留出与返回按钮相同的空间 */
+}
+
+.search-box {
+  width: 640rpx;
+  height: 80rpx;
+  background: #f0f0f0;
+  border-radius: 40rpx;
+  display: flex;
+  align-items: center;
+  padding: 0 30rpx;
+  position: relative;
+  margin-top: 30rpx;
+}
+
+.search-box icon {
+  margin-right: 15rpx;
+}
+
+.search-box input {
+  flex: 1;
+  height: 80rpx;
+  font-size: 30rpx;
+  color: #333;
+}
+
+.placeholder {
+  color: #999;
+  font-size: 30rpx;
+}
+
+.search-btn {
+  font-size: 32rpx;
+  color: #333;
+  padding: 0 10rpx;
+  border-left: 1px solid #ccc;
+  padding-left: 20rpx;
+}
+
+.search-result {
+  flex: 1;
+  height: calc(100vh - 180rpx);
+  width: 700rpx;
+  box-sizing: border-box;
+  margin-top: 30rpx;
+}
+
+.book-list {
+  margin-bottom: 20rpx;
+}
+
+.book-item {
+  display: flex;
+  padding: 20rpx;
+  background: #fff;
+  border-radius: 8rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
+}
+
+.book-cover {
+  width: 180rpx;
+  height: 240rpx;
+  border-radius: 8rpx;
+  margin-right: 20rpx;
+}
+
+.book-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+
+.book-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 10rpx;
+  line-height: 1.4;
+}
+
+.book-title .highlight {
+  color: #0675FF;
+  font-weight: bold;
+}
+
+.book-desc {
+  font-size: 26rpx;
+  color: #666;
+  margin-bottom: 10rpx;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 3;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.book-author {
+  font-size: 24rpx;
+  color: #999;
+}
+
+.loading, .no-more, .empty {
+  text-align: center;
+  padding: 30rpx 0;
+  color: #999;
+  font-size: 24rpx;
+}
+
+.empty {
+  padding: 100rpx 0;
+} 

+ 60 - 70
utils/request.js

@@ -20,6 +20,18 @@ switch (version) {
     break;
 }
 
+// 添加登录锁和请求队列
+let isLoggingIn = false;
+let loginPromise = null;
+let pendingRequests = [];
+
+// 执行等待队列中的请求
+const executePendingRequests = () => {
+  const requests = [...pendingRequests];
+  pendingRequests = [];
+  return Promise.all(requests.map(req => req()));
+};
+
 // 普通POST请求
 export const post = (url, body = {}) => {
   return new Promise((resolve, reject) => {
@@ -73,7 +85,15 @@ export function uploadTrackLog(url, body) {
 }
 
 export const requestLogin = (data) => {
+  console.log('开始登录请求,参数:', data);
   return new Promise((resolve, reject) => {
+    // 检查是否已经有token
+    const existingToken = wx.getStorageSync('accessToken');
+    if (existingToken) {
+      console.log('已存在token,跳过登录');
+      return resolve({ accessToken: existingToken });
+    }
+
     wx.request({
       url: httpServiceURL + '/user/loginWx',
       data: data,
@@ -82,31 +102,31 @@ export const requestLogin = (data) => {
       header: {},
       success: function (res) {
         if (res.data.code == 200) {
-          console.log(res);
+          console.log('登录成功,保存token');
+          wx.setStorageSync('accessToken', res.data.data.accessToken);
           resolve(res.data.data);
         } else {
+          console.log('登录失败,错误信息:', res.data);
           wx.showToast({
             title: res.data.msg || res.data.error || '登录失败...',
             icon: 'none',
           });
-          console.log('login fail', res);
           reject(res.data);
         }
       },
       fail: function (res) {
-        console.log('login fail', res);
+        console.log('登录请求失败:', res);
         wx.showToast({
           title: '登录失败...',
           icon: 'none',
         });
+        reject(res);
       },
     });
   });
 };
 
 export const requestAll = (requestUrl, body, method = 'POST', customHeaders = {}) => {
-  console.log("body",body)
-  let app = getApp().globalData;
   let url = httpServiceURL + requestUrl;
   if (method === 'GET') {
     if (body) {
@@ -120,6 +140,9 @@ export const requestAll = (requestUrl, body, method = 'POST', customHeaders = {}
   // 封装请求函数
   const doRequest = () => {
     return new Promise((resolve, reject) => {
+      // 优先使用传入的token,其次使用storage中的token
+      const token = customHeaders.token || wx.getStorageSync('accessToken') || '';
+
       wx.request({
         url: url,
         data: body,
@@ -127,12 +150,10 @@ export const requestAll = (requestUrl, body, method = 'POST', customHeaders = {}
         dataType: 'json',
         header: {
           ...customHeaders,
-          token: customHeaders.token || app.token || '',
+          token
         },
         success: function (res) {
           if (res.data.code == 200) {
-            // 是否需要解密
-            console.log(url, '<------>', res.data.data);
             if (typeof res.data.data === 'number') {
               resolve(res.data.data);
             } else {
@@ -140,11 +161,9 @@ export const requestAll = (requestUrl, body, method = 'POST', customHeaders = {}
             }
           } 
           else if(res.data.code==20001){
-            //令牌过期,重新登录
             reject({code: 20001, msg: '令牌过期'});
           }
           else {
-            console.log('error:', res.data, url);
             reject(res.data);
           }
         },
@@ -161,14 +180,40 @@ export const requestAll = (requestUrl, body, method = 'POST', customHeaders = {}
 
   // 执行请求,如果token过期则重新登录后重试
   return doRequest().catch(async error => {
-    if (error.code === 20001) {
-      // 重新登录
-      const app = getApp();
+    if (error.code == 20001) {
+      // 如果已经在登录中,将请求添加到队列
+      if (isLoggingIn) {
+        return new Promise((resolve, reject) => {
+          pendingRequests.push(() => doRequest().then(resolve).catch(reject));
+        });
+      }
+
+      // 如果没有在登录,开始登录流程
+      isLoggingIn = true;
+
       try {
-        await app.login();
-        // 重新执行请求
+        // 使用单例登录Promise
+        if (!loginPromise) {
+          wx.removeStorageSync('accessToken');
+          loginPromise = getApp().login();
+        }
+        await loginPromise;
+        
+        // 重置登录状态和Promise
+        isLoggingIn = false;
+        loginPromise = null;
+
+        // 执行所有请求
+        if (pendingRequests.length > 0) {
+          pendingRequests.push(() => doRequest());
+          return executePendingRequests();
+        }
         return doRequest();
       } catch (loginError) {
+        // 登录失败,重置状态
+        isLoggingIn = false;
+        loginPromise = null;
+        pendingRequests = [];
         console.error('重新登录失败:', loginError);
         throw loginError;
       }
@@ -176,58 +221,3 @@ export const requestAll = (requestUrl, body, method = 'POST', customHeaders = {}
     throw error;
   });
 };
-
-/**
- * 图文服务
- */
-export function tuwenRequest(url, body) {
-  if (!body) {
-    body = {};
-  }
-  return tuwenPost('https://kratos.mokamrp.com' + url, body);
-}
-
-export const tuwenPost = (url, body = {}) => {
-  const app = getApp()
-  return new Promise((resolve, reject) => {
-    wx.request({
-      url: url,
-      data: body,
-      method: 'POST',
-      dataType: 'json',
-      header: {
-        'Authorization': 'tanlongjiefuckdick'
-      },
-      success: (res) => {
-        if (res.statusCode == 200 && res.data.code == 200) {
-          let result = JSON.parse(res.data.data)
-          resolve(result)
-        } else {
-          reject(res)
-        }
-      },
-      fail: (res) => {
-        console.log(url, 'error--------', res)
-        reject(res)
-      }
-    })
-  })
-}
-
-/**
- * 投诉
- */
-export function workGet(url, body) {
-  if (!body) {
-    body = {}
-  }
-  return get('https://moka-volta-kratos-workwx-h5.mokamrp.com' + url, body)
-}
-
-export function workPost(url, body) {
-  if (!body) {
-    body = {}
-  }
-  return post('https://moka-volta-kratos-workwx-h5.mokamrp.com' + url, body)
-}
-

+ 40 - 1
utils/util.js

@@ -14,6 +14,45 @@ const formatNumber = n => {
   return n[1] ? n : `0${n}`
 }
 
+// 跳转到书籍详情页
+export const goToBookDetail = (options) => {
+  // 检查必要参数
+  if (!options || (!options.bookId || !options.wxBookId)) {
+    console.warn('书籍ID不能为空');
+    return;
+  }
+  
+  let url = '';
+  
+  // 根据不同参数构建不同的URL
+  if (options.wxBookId) {
+    // 微信书籍ID跳转
+    url = `plugin-private://wx293c4b6097a8a4d0/pages/novel/index?bookId=${options.wxBookId}`;
+    
+    // 如果有章节ID,添加到URL
+    if (options.chapterId) {
+      url += `&chapterId=${options.chapterId}`;
+    }
+
+    if (options.bookId) {
+      url += `&innerBookId=${options.bookId}`;
+    }
+  }
+  
+  wx.navigateTo({
+    url,
+    fail: (err) => {
+      console.error('跳转书籍详情页失败:', err);
+      console.log('url',url);
+      wx.showToast({
+        title: '跳转失败,请重试',
+        icon: 'none'
+      });
+    }
+  });
+};
+
 module.exports = {
-  formatTime
+  formatTime,
+  goToBookDetail
 }