Parcourir la source

实现小说阅读器插件集成,添加用户登录功能,优化首页数据加载,新增卡片小说和横幅展示,完善登录状态管理及界面交互,更新样式和请求工具函数。

yangwenlan il y a 1 an
Parent
commit
85cffd3b2e

+ 25 - 0
api/api.js

@@ -0,0 +1,25 @@
+import { requestAll, requestLogin } from '../utils/request'
+
+
+export const userLogin = (data) => requestLogin(data)
+
+// 获取token
+const getToken = () => {
+    return wx.getStorageSync('accessToken') || ''
+}
+
+// 添加token到header
+const addTokenToHeader = () => {
+    return {
+        token: getToken()
+    }
+}
+
+export const getBannerList = (channel) => {
+    return requestAll('/novel/banner', {channel:channel}, 'GET', addTokenToHeader())
+}
+
+// /novel/cardNovels
+export const getCardNovels = (channel) => {
+    return requestAll('/novel/cardNovels', {channel:channel}, 'GET', addTokenToHeader())
+}

+ 109 - 6
app.js

@@ -1,20 +1,123 @@
 // app.js
+// 引入阅读器插件
+const novelPlugin = requirePlugin('novel-plugin')
+import { userLogin } from './api/api'
+
 App({
   onLaunch() {
+    // 监听进入插件页事件
+    novelPlugin.onPageLoad(onNovelPluginLoad)
+
     // 展示本地存储能力
     const logs = wx.getStorageSync('logs') || []
     logs.unshift(Date.now())
     wx.setStorageSync('logs', logs)
+  },
 
-    // 登录
-    wx.login({
-      success: res => {
-        // 发送 res.code 到后台换取 openId, sessionKey, unionId
-        console.log(res);
+  // 登录方法
+  login() {
+    return new Promise((resolve, reject) => {
+
+      // 检查登录状态
+      if (this.checkLoginStatus()) {
+        resolve();
+        return;
       }
+
+      wx.login({
+        success: res => {
+          userLogin({
+            "appid": "wxa7a33088566e1292",
+            "channel": 1,
+            "srcType": "0",
+            "srcAppId": "",
+            "srcId": "",
+            "cusId": "",
+            "inviter": "",
+            "promotionId": "",
+            "inviterPromotionId": "",
+            "system": "pc",
+            "type": "inner",
+            "inner": 1,
+            code: res.code,
+          }).then(res => {
+            console.log(res);
+            wx.setStorageSync('accessToken', res.accessToken);
+            this.globalData.isLogin = true;
+            resolve(res);
+          }).catch(err => {
+            this.globalData.isLogin = false;
+            reject(err);
+          })
+        },
+        fail: err => {
+          this.globalData.isLogin = false;
+          reject(err);
+        }
+      })
     })
   },
+
+  // 检查登录状态
+  checkLoginStatus() {
+    const token = wx.getStorageSync('accessToken');
+    return !!token && this.globalData.isLogin;
+  },
+
   globalData: {
-    userInfo: null
+    userInfo: null,
+    isLogin: false
   }
 })
+
+// 插件初始化回调
+function onNovelPluginLoad(data) {
+  // data.id - 阅读器实例 id,每个插件页对应一个阅读器实例
+  const novelManager = novelPlugin.getNovelManager(data.id)
+  
+  // 设置目录状态(这里先模拟三章内容,实际应该根据后端数据设置)
+  novelManager.setContents({
+    contents: [
+      {
+        index: 0, // 第一章
+        status: 0, // 免费
+      },
+      {
+        index: 1, // 第二章
+        status: 2, // 未解锁
+      },
+      {
+        index: 2, // 第三章
+        status: 1, // 已解锁
+      }
+    ],
+  })
+
+  // 监听用户行为事件
+  novelManager.onUserTriggerEvent(res => {
+    const { event_id } = res;
+    console.log('用户行为:', event_id, res);
+
+    // 根据不同的事件类型处理
+    switch(event_id) {
+      case 'start_read': // 开始阅读
+        console.log('开始阅读章节:', res.chapter_id);
+        break;
+      
+      case 'leave_readpage': // 离开阅读页
+        console.log('阅读时长:', res.read_time);
+        break;
+      
+      case 'change_chapter': // 切换章节
+        console.log('切换到章节:', res.chapter_id);
+        break;
+      
+      case 'get_chapter': // 获取章节数据
+        console.log('章节状态:', res.pay_status);
+        break;
+      
+      default:
+        break;
+    }
+  })
+}

+ 18 - 4
app.json

@@ -11,12 +11,26 @@
   "window": {
     "navigationStyle": "custom",
     "backgroundColor": "#f6f6f6",
-    "backgroundTextStyle": "light"
+    "backgroundTextStyle": "light",
+    "navigationBarBackgroundColor": "#fff",
+    "navigationBarTitleText": "WeChat",
+    "navigationBarTextStyle": "black"
+  },
+  "plugins": {
+    "novel-plugin": {
+      "version": "latest",
+      "provider": "wx293c4b6097a8a4d0",
+      "genericsImplementation": {
+        "novel": {
+          "charge-dialog": "components/charge-dialog"
+        }
+      }
+    }
   },
   "tabBar": {
-    "color": "#999999",
-    "selectedColor": "#000",
-    "backgroundColor": "#ffffff",
+    "color": "#999",
+    "selectedColor": "#ff6b6b",
+    "backgroundColor": "#fff",
     "borderStyle": "black",
     "list": [
       {

+ 40 - 0
components/charge-dialog/index.js

@@ -0,0 +1,40 @@
+const novelPlugin = requirePlugin('novel-plugin')
+
+Component({
+  properties: {
+    novelManagerId: {
+      type: Number,
+      value: -1,
+    },
+    bookId: {
+      type: String,
+      value: '',
+    },
+    chapterIndex: {
+      type: Number,
+      value: -1,
+    },
+    chapterId: {
+      type: String,
+      value: '',
+    },
+    originalId: {
+      type: String,
+      value: '',
+    },
+  },
+
+  methods: {
+    unlock() {
+      // 取出对应的阅读器实例
+      const novelManager = novelPlugin.getNovelManager(this.properties.novelManagerId)
+
+      // do something
+      console.log("======解锁=====")
+      console.log(this.properties)
+
+      // 告诉阅读器这一章已解锁
+      novelManager.paymentCompleted()
+    },
+  },
+})

+ 4 - 0
components/charge-dialog/index.json

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

+ 1 - 0
components/charge-dialog/index.wxml

@@ -0,0 +1 @@
+<button bind:tap="unlock">解锁第 {{ chapterIndex + 1 }} 章</button>

+ 135 - 28
pages/index/index.js

@@ -1,28 +1,25 @@
 // index.js
 const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
 const testCover = '/assets/images/bg-book.png'
+import { getBannerList, getCardNovels } from '../../api/api'
+
+const app = getApp()
 
 Page({
   data: {
+    isLoggingIn: true, // 添加登录状态
     // 轮播配置
     indicatorDots: true,
     autoplay: true,
     interval: 3000,
     duration: 500,
+    bannerList: [], // 添加banner列表数据
     
     // 性别选项
     gender: 'male', // male or female
     
     // 主编推荐数据
-    recommendBooks: [
-      {
-        id: 1,
-        title: '恢复记忆后,她重归家门虐前夫',
-        desc: '江逸凌从一朝落难,天之骄女跌落神坛,得津南于绝邦之境;赐她姓名,赐以闯荡之名赐她他,...',
-        stats: '近期收藏3万',
-        cover: testCover
-      }
-    ],
+    recommendBooks: [], // 主编推荐数据
     
     // 推荐书籍列表
     bookList: [
@@ -49,24 +46,10 @@ Page({
     ],
     
     // 全网热推
-    hotBooks: [
-      {
-        id: 1,
-        title: '进荒陆上:小农女意外救国',
-        desc: '富宁于农母留半年,一起穿到了古代,遇上饥荒,她靠,战乱,开荒要逆命!人人都说这...',
-        stats: '14.3万人看过',
-        cover: testCover
-      },
-      {
-        id: 2,
-        title: '离婚后,绝色女总裁被求原谅',
-        desc: '他劝妻子成立公司,妻子名扬天下却抛弃他提出想离婚,却不知,他才是幕后的那个王者!',
-        stats: '42.3万人看过',
-        cover: testCover
-      }
-    ],
+    hotBooks: [], // 全网热推数据
     
     // 强力推荐
+    showStrongRecommend: false, // 控制强力推荐模块显示/隐藏
     strongBooks: [
       {
         id: 1,
@@ -101,11 +84,45 @@ Page({
   },
   
   onLoad() {
+    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;
+    }
     // 每次页面显示时同步性别状态
     this.syncGenderState();
   },
@@ -128,9 +145,13 @@ Page({
   // 切换性别分类
   switchGender: function(e) {
     const { gender } = e.detail;
+    // 如果性别没有改变,直接返回
+    if (this.data.gender === gender) {
+      return;
+    }
     this.setData({ gender });
     // 根据性别加载不同的书籍数据
-    this.loadGenderData(gender);
+    this.initPageData();
   },
   
   // 加载性别相关数据
@@ -154,9 +175,9 @@ Page({
   
   // 跳转到书籍详情页
   goToBookDetail: function(e) {
-    const bookId = e.currentTarget.dataset.id;
+    const bookId = 'A1HcfuuvKqNdTuMEfF4DMKXo5A'; // 暂时写死的书籍ID
     wx.navigateTo({
-      url: '/pages/book/detail?id=' + bookId
+      url: `plugin-private://wx293c4b6097a8a4d0/pages/novel/index?bookId=${bookId}`
     });
   },
   
@@ -233,4 +254,90 @@ Page({
       }
     })
   },
+  // 获取banner列表
+  async getBannerList() {
+    try {
+      const bannerList = await getBannerList(this.data.gender === 'male' ? 1 : 2);
+      this.setData({
+        bannerList
+      });
+    } catch (error) {
+      console.error('获取banner失败:', error);
+      wx.showToast({
+        title: '获取banner失败',
+        icon: 'none'
+      });
+    }
+  },
+
+  async getCardNovels() {
+    try {
+      const cardNovels = await getCardNovels(this.data.gender === 'male' ? 1 : 2);
+      // 默认使用第一组数据作为主编推荐
+      if (cardNovels[0]) {
+        const [firstBook, ...restBooks] = cardNovels[0].novelList;
+        this.setData({
+          recommendBooks: firstBook ? [{
+            id: firstBook.id,
+            title: firstBook.title,
+            desc: firstBook.brief,
+            stats: `近期收藏${firstBook.readingNumber}`,
+            cover: firstBook.cover
+          }] : [],
+          bookList: restBooks.slice(0, 4).map(novel => ({
+            id: novel.id,
+            title: novel.title,
+            cover: novel.cover
+          })),
+          editorTitle: cardNovels[0].name // 设置实际的标题
+        });
+      }
+      
+      // 第二组数据作为全网热推
+      if (cardNovels[1]) {
+        this.setData({
+          hotBooks: cardNovels[1].novelList.map(novel => ({
+            id: novel.id,
+            title: novel.title,
+            desc: novel.brief,
+            stats: `${novel.readingNumber}人看过`,
+            cover: novel.cover
+          })),
+          hotTitle: cardNovels[1].name // 设置实际的标题
+        });
+      }
+
+      // 第三组数据作为强力推荐
+      if (cardNovels[2] && cardNovels[2].novelList.length > 0) {
+        this.setData({
+          strongBooks: cardNovels[2].novelList.slice(0, 4).map(novel => ({
+            id: novel.id,
+            title: novel.title,
+            cover: novel.cover
+          }))
+        });
+      } else {
+        // 如果没有第三组数据,隐藏强力推荐模块
+        this.setData({
+          strongBooks: []
+        });
+      }
+    } catch (error) {
+      console.error('获取卡片小说失败:', error);
+      wx.showToast({
+        title: '获取卡片小说失败',
+        icon: 'none'
+      });
+    }
+  },
+
+  // 处理banner点击
+  onBannerTap(e) {
+    const banner = e.currentTarget.dataset.item;
+    if (banner.linkUrl) {
+      wx.navigateTo({
+        url: banner.linkUrl
+      });
+    }
+  },
 })

+ 29 - 11
pages/index/index.wxml

@@ -1,6 +1,14 @@
 <!--index.wxml-->
 <view class="container">
 
+  <!-- 登录加载提示 -->
+  <view class="loading-mask" wx:if="{{isLoggingIn}}">
+    <view class="loading-content">
+      <view class="loading-icon"></view>
+      <text>登录中...</text>
+    </view>
+  </view>
+
   <!-- 头部区域 -->
   <!-- <view class="header">
     <view class="avatar">
@@ -8,7 +16,7 @@
     </view>
   </view> -->
 
-  <view class="main-wrapper">
+  <view class="main-wrapper" wx:if="{{!isLoggingIn}}">
     <!-- 性别切换和搜索区域 -->
     <view class="tab-search">
       <gender-tabs gender="{{gender}}" bind:switch="switchGender"/>
@@ -19,8 +27,18 @@
     </view>
 
     <!-- 主Banner区域 -->
-    <view class="main-banner">
-      <image src="/assets/images/bg-book.png" mode="aspectFill"></image>
+    <view wx:if="{{bannerList.length > 0}}" class="main-banner">
+      <swiper 
+        indicator-dots="{{indicatorDots}}"
+        autoplay="{{autoplay}}"
+        interval="{{interval}}"
+        duration="{{duration}}"
+        circular="true"
+        class="banner-swiper">
+        <swiper-item wx:for="{{bannerList}}" wx:key="id">
+          <image src="{{item.imageUrl}}" mode="aspectFill" bindtap="onBannerTap" data-item="{{item}}"/>
+        </swiper-item>
+      </swiper>
     </view>
 
     <!-- 功能区 -->
@@ -44,17 +62,17 @@
   <!-- 主编推荐 -->
   <view class="recommend-section">
     <view class="section-header">
-      <text class="title">主编推荐</text>
+      <text class="title">{{editorTitle || '编辑推荐'}}</text>
       <text class="more" bindtap="goToFeature" data-feature="editor">更多 ></text>
     </view>
 
     <view class="recommend-main">
-      <view class="recommend-large" wx:for="{{recommendBooks}}" wx:key="id" bindtap="goToBookDetail" data-id="{{item.id}}">
-        <image src="{{item.cover}}" mode="aspectFill"></image>
+      <view class="recommend-large" wx:if="{{recommendBooks[0]}}" bindtap="goToBookDetail" data-id="{{recommendBooks[0].id}}">
+        <image src="{{recommendBooks[0].cover}}" mode="aspectFill"></image>
         <view class="book-info">
-          <view class="book-title">{{item.title}}</view>
-          <view class="book-desc">{{item.desc}}</view>
-          <view class="book-stats">{{item.stats}}</view>
+          <view class="book-title">{{recommendBooks[0].title}}</view>
+          <view class="book-desc">{{recommendBooks[0].desc}}</view>
+          <view class="book-stats">{{recommendBooks[0].stats}}</view>
         </view>
       </view>
     </view>
@@ -70,7 +88,7 @@
   <!-- 全网热推 -->
   <view class="hot-section">
     <view class="section-header">
-      <text class="title">全网热推</text>
+      <text class="title">{{hotTitle || '热门推荐'}}</text>
       <text class="more" bindtap="goToFeature" data-feature="hot">更多 ></text>
     </view>
 
@@ -87,7 +105,7 @@
   </view>
 
   <!-- 强力推荐 -->
-  <view class="strong-recommend">
+  <view class="strong-recommend" wx:if="{{strongBooks.length>0}}">
     <view class="section-header">
       <text class="title">强力推荐</text>
       <text class="more" bindtap="goToFeature" data-feature="recommend">更多 ></text>

+ 45 - 3
pages/index/index.wxss

@@ -9,7 +9,7 @@ page {
 .container {
   width: 100%;
   min-height: 100vh;
-  padding-bottom: 100rpx;
+  padding-bottom: 30rpx;
   box-sizing: border-box;
   position: relative;
 }
@@ -194,6 +194,8 @@ page {
   background-color: #fff;
   margin: 20rpx 30rpx 30rpx;
   border-radius: 20rpx;
+  width: 690rpx;
+  overflow:hidden;
 }
 
 .recommend-main {
@@ -256,8 +258,8 @@ page {
 }
 
 .book-item image {
-  width: 100%;
-  height: 220rpx;
+  width: 125rpx;
+  height: 180rpx;
   border-radius: 10rpx;
   margin-bottom: 10rpx;
 }
@@ -402,3 +404,43 @@ page {
 .tab-item.active text {
   color: #ff5252;
 }
+
+/* 登录加载遮罩 */
+.loading-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(255, 255, 255, 0.9);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 9999;
+}
+
+.loading-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.loading-icon {
+  width: 80rpx;
+  height: 80rpx;
+  margin-bottom: 20rpx;
+  border: 6rpx solid #f3f3f3;
+  border-top: 6rpx solid #07c160;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+}
+
+.loading-content text {
+  font-size: 28rpx;
+  color: #666;
+}
+
+@keyframes spin {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}

+ 207 - 0
utils/request.js

@@ -0,0 +1,207 @@
+// 预发布地址
+let httpTrackLog = 'https://hyperion-track-app.mokamrp.com';
+let httpServiceURL = '';
+let version = __wxConfig.envVersion;
+switch (version) {
+  case 'develop':
+    httpServiceURL = 'https://hyperion-novel-qa-app.mokamrp.com';
+    // httpServiceURL = "http://172.16.24.193:10111";
+    // httpServiceURL = 'https://iaa-app.mokamrp.com';
+    break;
+  case 'trial':
+    httpServiceURL = 'https://test-iaa-app.mokamrp.com';
+    // httpServiceURL = 'https://iaa-app.mokamrp.com';
+    break;
+  case 'release': //正式版
+    httpServiceURL = 'https://iaa-app.mokamrp.com';
+    break;
+  default: //未知,默认调用正式版
+    httpServiceURL = 'https://iaa-app.mokamrp.com';
+    break;
+}
+
+// 普通POST请求
+export const post = (url, body = {}) => {
+  return new Promise((resolve, reject) => {
+    wx.request({
+      url: url,
+      data: body,
+      method: 'POST',
+      dataType: 'json',
+      success: (res) => {
+        if (res.statusCode == 200 && res.data.code == 200) {
+          resolve(res.data.data);
+        } else {
+          reject(res);
+        }
+      },
+      fail: (res) => {
+        console.log(url, 'error--------', res);
+        reject(res);
+      },
+    });
+  });
+};
+
+// 普通Get请求
+export const get = (url) => {
+  return new Promise((resolve, reject) => {
+    wx.request({
+      url: url,
+      method: 'GET',
+      success: (res) => {
+        if (res.statusCode == 200 && res.data.code == 200) {
+          resolve(res.data);
+        } else {
+          reject(res);
+        }
+      },
+      fail: (res) => {
+        reject(res);
+      },
+    });
+  });
+};
+/**
+ * 打点请求
+ */
+export function uploadTrackLog(url, body) {
+  if (!body) {
+    body = {};
+  }
+  return post(httpTrackLog + url, body);
+}
+
+export const requestLogin = (data) => {
+  return new Promise((resolve, reject) => {
+    wx.request({
+      url: httpServiceURL + '/user/loginWx',
+      data: data,
+      method: 'POST',
+      dataType: 'json',
+      header: {},
+      success: function (res) {
+        if (res.data.code == 200) {
+          console.log(res);
+          resolve(res.data.data);
+        } else {
+          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);
+        wx.showToast({
+          title: '登录失败...',
+          icon: 'none',
+        });
+      },
+    });
+  });
+};
+
+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) {
+      url += '?';
+      for (const k in body) {
+        url += `&${k}=${body[k]}`;
+      }
+    }
+  }
+
+  return new Promise((resolve, reject) => {
+    wx.request({
+      url: url,
+      data: body,
+      method: method,
+      dataType: 'json',
+      header: {
+        ...customHeaders,
+        token: customHeaders.token || app.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 {
+            resolve(res.data.data || []);
+          }
+        } else {
+          console.log('error:', res.data, url);
+          reject(res.data);
+        }
+      },
+      fail: function (res) {
+        wx.showToast({
+          title: '网络出错,请重试...',
+          icon: 'none',
+        });
+      },
+    });
+  });
+};
+
+/**
+ * 图文服务
+ */
+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)
+}
+