vue+node+mysql+epubjs全栈开发之移动端小说app

2,483 阅读6分钟

一 : 简介与说明

1.1 简介

这是小编写的第一款全栈程序,可能有些许不足,还望海涵. 写这款程序的主要意图是使自己能更加熟悉以及掌握vue,node,等相关的知识以及发现自己的不足并改进.这篇文章将会从爬虫说起,到后端的简介以及重点讲解前端.

项目与开源至github 地址:

github.com/1131446340a…

下面是阅读界面的动图.

1.2项目的基本要求

会使用vue,node,mysql,html,css3,vant,并对其有一定的基本认识.

1.3 核心插件

epubjs:对epub格式小说的解析

axios:发起网络请求

puppeteer:谷歌开源的无头浏览器,可用来做测试与爬虫

cheerio:对html内容进行解析

stylus: 更加方便书写css样式

vant:vue移动端ui框架

better-scroll : 移动端实现流畅的滚动

koa2:开源node框架

koa2-cors: 实现跨域,

mysql : 连接mysql数据库

1.4项目的基本配置

准备三个文件夹分别对应爬虫,node后端,vue前端,并在对应的文件夹下安装好相应的插件

二: 爬虫

目标网站:sobooks.cc/

首先我们先引入模块

let puppeteer = require("puppeteer")
let $ = require('cheerio')
let url = require('url')
let write = require('../06demo-fs/index.js')

声明目标地址变量

let httpurl = "https://sobooks.cc/shenghuoshishang"

这里用sobooks网站的一个生活时尚分类作为模块,其他模块只要修改一下地址就行

无头浏览器的配置

let brower = await puppeteer.launch({
        headless: true
    })

headless为true则代表不打开浏览器,false则打开

  async function getAllnum() {
        let page = await brower.newPage()  //打开一个新的页面
        await page.setRequestInterception(true)  //拦截打开
        await page.on('request', Interception => {
            let urlobj = url.parse(Interception.url())
            if (urlobj.hostname == 'googleads.g.doubleclick.net') {
                Interception.abort() // 对谷歌广告进行拦截,提高爬取速率
            } else {
                Interception.continue()
            }
        })
        await page.goto(httpurl) //前往目标地址
        let pageNum = await page.$eval('.pagination li:last-child span', element => {
            let text = element.innerHTML.substring(1, element.innerHTML.length - 2).trim()
            return text
        }) // 使用page的$eval方法选中指定元素,并对其操作,获得总页数
        page.close()
        return pageNum;
    }

创建一个读取总页数的函数,代码具体作用请观看注释


    async function pageList(num) {
        let pagelisturl = await "https://sobooks.cc/shenghuoshishang/page/" + num
       
        let page = await brower.newPage()
        await page.setRequestInterception(true)
        await page.on('request', Interception => {
            let urlobj = url.parse(Interception.url())
            if (urlobj.hostname == 'googleads.g.doubleclick.net') {
                Interception.abort()
            } else {
                Interception.continue()
            }
        })
        await page.goto(pagelisturl)
        let arrpage = await page.$$eval(".card .card-item .thumb-img>a", elements => {
            let arr = []
            elements.forEach((item, i) => {
                let href = item.getAttribute('href')
                arr.push(href)
            })
            return arr
        })
        // console.log(arrpage);
        page.close()
        return arrpage
    }

创建一个函数获取当前页面的所有href,前面的函数获取到总页数,对所有页数进行遍历即可得到每项的href,然后我们可以通过这个href去往小说详情页面,然后对其进行爬取

注:?eval与$eval的区别:

相当于?eval 选中所有符合要求的元素,$eval 只选中一个

后面操作类似,无非就是选中元素,并对所选元素进行加工处理,然后通过node 的write模块将其写入到本地json文件

github源码地址 github.com/1131446340a…

代码写入json的时候为了方便,将json内容格式写入为'{},{},'请人为改成[{},{}]这种格式

json文件如图:

其中urls 关键字为下载地址的关键网址

然后再对下载网址进行爬取,github源码地址 github.com/1131446340a…

其中对爬取小说做了拦截,比如下载需时超过10分钟的就拦截了.基本没什么难度,通过流式下载即可.

最后将json文件通过mysql存入数据库就行.

三 : 后端接口

首先先连接mysql数据库

var mysql = require('mysql')
var config = require('./defaultConfig')

// 创建线程池
var pool = mysql.createPool({
    host: config.database.HOST,
    user: config.database.USERNAME,
    password: config.database.PASSWORD,
    database: config.database.DATABASE,
    port: config.database.PORT
})

// 统一连接数据库的方法
let allServies = {
    query: function (sql, values) {
        return new Promise((resolve, reject) => {
            pool.getConnection(function (err, connection) {
                if (err) {
                    reject(err)
                } else {
                    connection.query(sql, values, (err, rows) => {
                        if (err) {
                            reject(err)
                        } else {
                            resolve(rows)
                        }
                        connection.release()
                    })

                }
            })
        })
    }
}

然后在该文件下写入mysql语句,部分如下

其次解决koa2跨域问题,只要在app.js设置一下代码即可

var cors = require('koa2-cors');
var app = new Koa();
app.use(cors());

然后在routes下建立一个book的js文件

const router = require('koa-router')() 实例化koa路由
const booksServies = require('../controllers/mySqlConfig')引入mysql
router.prefix('/books')  //设置路由前缀

最后记得在APP.js中加上这段代码

app.use(book.routes(), book.allowedMethods())

然后通过router.get或者post方法定义后端接口即可.

形式如下

router.post('/bookinfo', async (ctx, next) => {
    let bookid = await ctx.request.body.params.bookid
    await booksServies.querybookinfo(bookid).then(res => {
        ctx.body = {
            bookinfo: res
        }
    })
})

其中ctx.request.body.params.bookid为前端发送参数

ctx.body内容为向前端展示的数据

做好以上操作,前端只需输入 http://localhost:3001/books/bookinfo 即可获取book的详情,当然,要携带参数bookid 注:其中端口号3001由自己在koa中设置

其他接口大多类似,不做详解

四 : vue前端

4.1 axios 的配置

import Vue from 'vue'
import axios from 'axios'
const vue = new Vue
Vue.config.devtools = true
axios.defaults.timeout = 5000; // 默认5s超时
// axios.defaults.baseURL = 'http://localhost:3000';
let urls = "http://localhost:3001/books"

Vue.prototype.$http = axios
// Vue.config.productionTip = false
function fetch(methods,url, param) {
  return new Promise((resolve, reject) => {
    axios[methods](urls + url, {
      params: param
    })
      .then(response => {
        resolve(response.data)
      }, err => {
        vue.$notify("网络出错或链接过期");
        reject(err)
      })
      .catch((error => {
        // console.log(this);

        reject(error)
      }))
  })
}

首先封装一个fetch方法

export function fetchGet(url,param){
return fetch('get',url,param)
}
export function fetchPost(url,param){
  return fetch('post',url,param)
  }

然后再拆分成get,和post两个方法,方便调用.

export function booksrore(fn) {
  return fetchGet("/bookstore").then(fn)
}

然后导出,这样其他页面只要引入然后直接booksrore(fn)就能调用,其中fn为回调函数

4.2路由的配置

首先引入

import Vue from 'vue'
import Router from 'vue-router'

然后引入组件

import index from '@/components/index'
import bookinfo from '@/components/common/bookinfo'
import reader from '@/components/common/reader'
import search from '@/components/search/search'
import my from '@/components/my/my'
import login from '@/components/my/login'
import zhuche from '@/components/my/zhuche'
import book from '@/components/book/book'
import bookstore from '@/components/bookstore/bookstore'
import readerHis from '@/components/book/readerHis'
import morebook from '@/components/bookstore/morebook'
import recommed from '@/components/recommed/recommed'

再使用路由

Vue.use(Router)

最后导出路由即可

    {
      path: '/',
      name: 'index',
      component: index,
      children: [
        {
          path: '/',                  // 子路由重定向
          redirect: 'bookstore'
        },
        {
          path: 'bookstore',
          name: 'bookstore',
          component: bookstore,
          meta:{
            requireAuth:true,
            keepAlive:true,
            isBack:true
          }
        },
        {
          path: '/recommed',
          name: 'recommed',
          component: recommed,
          meta:{
            requireAuth:true,
            keepAlive:true,
            isBack:true
          }
        },
        {
          path: 'my',
          name: 'my',
          component: my,
          meta:{
            requireAuth:true,
            keepAlive:true,
            isBack:true
          }
        },
        {
          path: 'book',
          name: 'book',
          component: book,
          meta:{
            requireAuth:true,
            keepAlive:true,
            isBack:true
          }
        },
      ],
      meta:{
        requireAuth:true,
        keepAlive:true,
        isBack:true
      }
     
    },

其中path为路由路径, component为引入的组件 ,children为子路由,meta为路由参数.keepAlive为一个参数,主要用来控制路由前进刷新页面,后退不刷新页面的控制条件.默认为true.

在app.vue下配置

 <keep-alive >
    <router-view v-if="$route.meta.keepAlive"></router-view>
    </keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>

用keep-alive组件控制router活性,被keep-alive包裹的路由是不会刷新页面的,如果让某页面刷新在路由跳转的时候调用生命周期beforeRouteLeave方法即可.调用如下

  beforeRouteLeave(to, from, next) {
    if(to.path=="/reader") {to.meta.keepAlive = false;
    }
    else{to.meta.keepAlive = true;}
    next();
  },

其中to,为要去的页面,from为当前页面,next为跳转函数.

例如上述代码代表的意思就是如果前往的路由接口是reader,则刷新页面,否则则保持活性( 因为这个页面只会前往reader路由,其他路由为后退路由,则做到了前进刷新页面,后退不刷新页面 )

4.3 index页面的搭建

效果图如下

顶部一个搜索框,底部一个导航栏,中间是四个二级路由分别代表的是书城,书架,精选,我的,四个页面

其中顶部的搜索框使用vant ui框架(vant的使用请观看vant官网)

中间部分稍后详说.

底部navbar代码如下

<template>
  <div class="main">
    <van-tabbar active-color="#000">
      <van-tabbar-item to="/bookstore">
        <div class="icon">
          <i
            class="iconfont icon-shucheng fontcolor"
            :class="{fontcoloractive:Route==='bookstore'}"
          ></i>
        </div>
        <div :class="{fontcoloractive:Route==='bookstore'}">书城</div>
      </van-tabbar-item>
      <van-tabbar-item to="/recommed">
        <div class="icon">
          <i
            class="iconfont icon-classify2-o fontcolor"
            :class="{fontcoloractive:Route==='recommed'}"
          ></i>
        </div>
        <div :class="{fontcoloractive:Route==='recommed'}">精选</div>
      </van-tabbar-item>
      <van-tabbar-item to="/book">
        <div class="icon">
          <i class="iconfont icon-shujia fontcolor" :class="{fontcoloractive:Route==='book'}"></i>
        </div>
        <div :class="{fontcoloractive:Route==='book'}">书架</div>
      </van-tabbar-item>
      <van-tabbar-item to="/my">
        <div class="icon">
          <i class="iconfont icon-sself fontcolor" :class="{fontcoloractive:Route==='my'}"></i>
        </div>
        <div :class="{fontcoloractive:Route==='my'}">我的</div>
      </van-tabbar-item>
    </van-tabbar>
  </div>
</template>

<script>
export default {
  name: "navbar",
  data() {
    return {
      Route: ""
    };
  },
  mounted() {
    this.Route = this.$route.name;
  },
  watch: {
    $route(route) {
      this.Route = route.name;
      console.log(this.Route);
    }
  }
};
</script>

开始我是使用页面索引(即1,2,3,4)作为判断条件fontcoloractive是否为真,后来发现这个页面有二级路由,如果从其他页面返回,二级路由不会刷新,但是一级路由会刷新,即顶部与底部navbar,导致返回之后页面的索引重新初始化,即navbar的活跃图片为第一个.后来使用watch监听$route,用route作为判断,即可完美解决返回navbar刷新导致底部活跃图标为第一个的bug.

注:其中icon图标来源于iconfont.

4.3.1 二级路由bookstore页面的搭建

主要代码

import { booksrore } from "../../network/index";
import scroll from "../common/scroll";
 getBook() {
      booksrore(res => {
        // console.log(
        res.content = res.content.sort(() => {
          return Math.random() - 0.5;
        });
        let xiaoshuowenxue = res.content.slice(0, 6);
        res.history = res.history.sort(() => {
          return Math.random() - 0.5;
        });
        let history = res.history.slice(0, 6);
        res.cglz = res.cglz.sort(() => {
          return Math.random() - 0.5;
        });
        let cglz = res.cglz.slice(0, 6);
        res.hightStar_select = res.hightStar_select.sort(() => {
          return Math.random() - 0.5;
        });
        let hightStar_select = res.hightStar_select.slice(0, 6);
        this.noval = [xiaoshuowenxue, history, cglz, hightStar_select];
        // this.data.xiaoshuowenxue = res.content.slice(0,6);
      });
    }

引入axios下的bookstore方法进行调用,可获得后端提供数据,将得到的数据进行随机排序,这样就可以做到下拉刷新页面.

效果如下

点击更多按钮会去往更多界面,点击单个小说会去往小说详情页面.

4.3.2 精选页面

同样通过bookstore方法获取数据,然后对其进行页面渲染,css没碰到什么技术痛点,所以展示关键代码

    getBook() {
      booksrore(res => {
        let xiaoshuowenxue = res.content.slice(0, 3);
        xiaoshuowenxue.forEach(item => {
          item.novel_content = JSON.parse(item.novel_content);
        });
        let history = res.history.slice(0, 3);
        history.forEach(item => {
          item.novel_content = JSON.parse(item.novel_content);
        });
        let cglz = res.cglz.slice(0, 3);
        cglz.forEach(item => {
          item.novel_content = JSON.parse(item.novel_content);
        });
        let shenghuo = res.shenghuo.slice(0, 3);
        shenghuo.forEach(item => {
          item.novel_content = JSON.parse(item.novel_content);
        });
        let renwen = res.renwen.slice(0, 3);
        renwen.forEach(item => {
          item.novel_content = JSON.parse(item.novel_content);
        });
        this.noval = [xiaoshuowenxue, history, cglz, renwen, shenghuo];
        // this.data.xiaoshuowenxue = res.content.slice(0,6);
      });
    },

获取数据并加工代码.

具体效果如下:

其中底部轮播页面是使用vant框架,所以也没什么难度,手写的话,起原理基本和轮播图的实现差不多.

4.3.3 书架页面

无登录和无收藏书籍的状态

登录状态基本也是使用vant. 因为这些页面无过多js操作,不做详说. 唯一要注意的是,如果处于无登录状态,不能调用和登录有关的接口即可.

关键代码:

    showcollect() {
      if (localStorage.book_user) {
        sqlcollection(
          res => {
            this.book = res.data;
            // console.log(res.data);
          },
          {
            user: localStorage.book_user
          }
        );
      }
      sqlcollection(
        res => {
          this.book = res.data;
          // console.log(res.data);
        },
        {
          user: localStorage.book_user
        }
      );
    },

效果动态图:

4.3.4 我的页面

登录与无登录状态图

登录之后会在本地保存一个book_user字段,根据此字段控制是否展示与隐藏某些内容.

4.4 登录与注册

注册于登录极其类似,因此只讲注册

  methods: {
    tologin() {
      this.$router.push({ path: "/login" });
    },
    zhuche() {
      if (!this.user) {
        this.$toast("手机号不能为空");
      } else {
        if (!/^1[3456789]\d{9}$/.test(this.user)) {
          this.$toast("手机号出错");
          return false;
        } else {
          if (this.pass.length < 6) this.$toast("密码至少6位");
          else {
            zhuche(
              res => {
                if (res.status == "200") {
                  this.$toast(res.msg);
                  localStorage.book_user = this.user;
                  this.$router.push({ path: "/my" });
                }
                if (res.status == "500") {
                  this.$toast(res.msg);
                }
              },
              { user: this.user, pass: this.pass }
            );
          }
        }
      }
    }
  },

首先判断手机号是否为空(手机号就是用户名)如果为空,则提示手机号为空,否则对输入的内容进行正则匹配.是否为手机号,如果不是手机号,提示手机号格式错误,最后检验当前号码是否已经被注册,如果已经注册提示用户已经被注册,否则进行注册,并调转到/my页面.

ui界面如下:

4.5 更多书籍页面

部分主要代码

  if (this.$route.query.catogry == "小说文学  精选好书") {
          if (20 * this.page < res.content.length)
            this.book = res.content.slice(0, 20 * this.page);
          else this.book = res.content;
          this.book.forEach(item => {
            item.novel_content = JSON.parse(item.novel_content);
          });
          // this.book.novel_content=JSON.parse(this.book.novel_content)
        }

数据源增加一个page字段,每次调用函数,page+1,以此控制当前页面显示内容的多少.

ui界面如下:

4.6搜索功能

ui界面如图:

历史记录采用行内块布局,默认显示一行,超出隐藏,点击向下icon图标即可打开,然后就会展示向上icon图标,点击则会关闭.点击删除icon则会删除所有历史记录.另外无登录状态没有历史记录,因为历史记录我设计的后端和用户挂钩.热搜则采用垂直风格布局.点击历史记录和热搜里面的内容可直接搜索,输入框内容搜索有一个400ms的防抖,当输入框有值时隐藏历史记录和热搜榜,展示搜索到的数据.点击搜索到的选项进行页面跳转,跳转到书籍详情页面,同时增加搜索记录,内容为输入框的值,如果存在则更新.不存在则插入.

动态效果展示图:

部分代码如下,防抖函数的实现原理就是设置一个定时器,如果再次执行函数则会清除定时器重新计时

   debounce(func, wait) {
      let timeout = null;
      return function() {
        if (timeout) {
          clearTimeout(timeout);
        }

        timeout = setTimeout(() => {
          func.apply(this, arguments);
        }, wait);
      };
    },
    tobookdetail(bookid) {
      if (bookid) {
        if (this.value) {
          if (localStorage.book_user) {
            insertHS(
              res => {
                console.log(res);
              },
              { text: this.value, user: localStorage.book_user }
            );
          }
        }
        this.$router.push({
          path: "/bookinfo",
          query: { bookid: bookid }
        });
      }
    },

4.7 书籍详情页面的开发

界面:

顶部一个返回栏是一个主键,其中间和右边内容为props传值,右边点击事件为emit发送.背景色为书籍图片的虚化.中间为书籍界面的简单布局.简介有内容简介和作者简介.内容简介通过点击按钮进行打开,打开后点击内容会进行收藏,另外还有一个目录通过for循环遍历展示.点击麻烦阅读按钮会去往读书界面,同时阅读历史增加一条数据.进入页面的时候回进行数据库查询本数据是否已收藏,根据查询结果显示不同的内容.

由于处于开发阶段,没有存放过多小说,小说内容只有一本,是写死的.意思就是不敢进入哪本小说都是固定的那一本小说.

      import Epub from "epubjs";
      this.book = new Epub("../../../static/巴别塔之犬.epub");
      this.book.ready
        .then(() => {
          // 生成目录
          this.navigation = this.book.navigation;
          // console.log(this.navigation);
          // 生成Locations对象
          return this.book.locations.generate();
        })
        

通过引入epubjs模块,然后通过调用ready()函数生成一个promise模块,然后通过this.book.navigation可生成目录信息,然后将其保存然后进行页面展示即可

4.8 阅读界面的开发

功能: 点击页面左侧向前翻页.点击页面右侧向后翻页.点击中间则打开设置. 设置可以修改字号,背景色,并对其进行记录,下次进入依旧生效.用户可以通过目录进行页面跳转,对于已经观看了的小说,会记录阅读进度.

页面效果开始已经上传了,这里就不上传了.

  mounted() {
    // console.log(this.$route.query.href);
    if (localStorage.book_user) {
      sqlCll(
        res => {
          // console.log(res);
          console.log(res);

          if (res.status === "200") {
            this.right = "已加入书架";
          }
          if (res.status === "500") {
            this.right = "加入书架";
          }
        },
        {
          user: localStorage.book_user,
          bookid: this.$route.query.bookid
        }
      );
      sqlprogress(
        res => {
          if (res.status == "200") {
            this.progress = res.data[0].progress;
          }
        },
        {
          user: localStorage.book_user,
          bookid: this.$route.query.bookid
        }
      );
      getBookInfo(
      res => {
        this.Bookinfo = res.bookinfo;
      },
      { bookid: this.$route.query.bookid }
    );
    }

    this.bookid = this.$route.query.bookid;
    
    this.getbook();
  },

首先在进入页面的时候,判断该书是否已经加入书架,然后通过不同的判断条件进行不同的页面显示, 然后读取book信息.同时还有个判断这本书籍用户是否看过,如果看过这将观看进度重新赋值. 最后调用getbook函数

    getbook() {
      // let title = this.Bookinfo[0].title;
      // let url = "../../../static/" + title + ".epub";
      // this.book = new Epub(url);
      this.book = new Epub("../../../static/巴别塔之犬.epub");
      this.rendition = this.book.renderTo("read", {
        width: window.innerWidth,
        height: window.innerHeight,
        method: "default"
      });
      // this.rendition.display();
      this.theme = this.rendition.themes;
      this.registerTheme();
      this.rendition.hooks.content.register(function(contents, view) {
        var elements = contents.document.querySelector("body");
        elements.style.lineHeight = "40px";
      });
      if(localStorage.book_user){sqlset(
        res => {
          if (localStorage.book_user) {
            if (res.status == "200") {
              this.bgc = res.data[0].bgcolor;
              let color = "";
              if (this.bgc == "#fff") {
                color = "white";
                this.actived = 0;
                this.$refs.main.style.color = "black";
              }
              if (this.bgc == "#FFFFF0") {
                color = "yello";
                this.actived = 1;
                this.$refs.main.style.color = "black";
              }
              if (this.bgc == "#E0FFFF") {
                color = "blue";
                this.actived = 3;
                this.$refs.main.style.color = "black";
              }
              if (this.bgc == "#000") {
                color = "black";
                this.actived = 4;
                this.$refs.main.style.color = "white";
              }
              if (this.bgc == "rgba(152, 251, 152, 0.8)") {
                color = "green";
                this.actived = 2;
                this.$refs.main.style.color = "black";
              }
              this.fontsize = res.data[0].fontsize;

              this.theme.select(color);
              this.theme.fontSize(this.fontsize + "px");
            }
          } else {
            this.actived = 1;
            this.theme.select("yello");
          }
        },
        { user: localStorage.book_user }
      );}
      this.book.ready
        .then(() => {
          // 生成目录
          this.navigation = this.book.navigation;
          // console.log(this.navigation);

          // 生成Locations对象
          return this.book.locations.generate();
        })
        .then(result => {
          // 保存locations对象
          this.locations = this.book.locations;
          // 标记电子书为解析完毕状态
          console.log(this.locations);

          this.bookAvailable = true;
          if (this.$route.query.href) {
            this.rendition.display(this.$route.query.href);
          } else {
            this.rendition.display(
              this.locations.cfiFromPercentage(this.progress)
            );
          }
        });
      // this.rendition.display();
    },

通过this.book.renderTo方法设置book显示的大小,read为所挂载的id,直接将显示宽高设置为屏幕宽高即可.

this.rendition.hooks.content.register方法可以对书籍页面的标签进行选择,这里选择body标签,将行高设置为40px.

然后调用用户设置接口,根据用户的设置信息展示用户设置好的信息.使用this.book.locations对小说进行解析. bookAvailable字段默认为false,解析完毕设置为true.然后通过this.rendition.display()方法进行页面展示. this.locations.cfiFromPercentage()方法接受一个数字,其值为0-1,可以定位到小说的百分比内容.

给整个页面添加一个mask盒子,进行绝对定位,其大小为这个屏幕,用弹性布局对其进行切分成三块,分别对应三个事件,前进后退以及打开设置.

下面对进行下一页进行说明

    tolast() {
      if (this.rendition) {
        if (localStorage.book_user) this.saveprogress();
        this.rendition.next();
        this.isShow = false;
      }
    }

this.rendition有一个next方法,调用次方法则可进行下一页.同时将设置界面关闭.

同时如果用户登录了,则调用saveprogreess方法对当前进度进行保存

    saveprogress() {
      var currentLocation = this.rendition.currentLocation();
      var progress =
        Math.floor(
          this.locations
            .percentageFromCfi(currentLocation.start.cfi)
            .toFixed(5) * 10000
        ) / 10000;
      this.progress = progress;
      Progress(
        res => {
          console.log(res);
        },
        {
          user: localStorage.book_user,
          bookid: this.$route.query.bookid,
          progress: progress
        }
      );
    },

this.locations .percentageFromCfi(currentLocation.start.cfi).toFixed(5) * 10000可以获取到当前页面所在进度. 然后调用接口将其存入后端即可.

  registerTheme() {
     this.themeList.forEach(theme => {
       this.theme.register(theme.name, theme.style);
     });
   },

通过registerTheme方法对主题进行注册,其中themeList为一个数组,结构如下:

  themeList: [
        {
          name: "white",
          style: {
            body: {
              background: "#fff",
              color: "#000"
            }
          }
        },
        {
          name: "black",
          style: {
            body: {
              background: "#000",
              color: "#fff"
            }
          }
        },
        {
          name: "yello",
          style: {
            body: {
              background: "#FFFFF0",
              color: "#000"
            }
          }
        },
        {
          name: "green",
          style: {
            body: {
              background: "rgba(152, 251, 152, 0.8)",
              color: "#000"
            }
          }
        },
        {
          name: "blue",
          style: {
            body: {
              background: "#E0FFFF",
              color: "#000"
            }
          }
        }
      ],

然后t通过 this.theme.select(color)方法进行选择主题,其中color参数为注册主题的name字段, 当改变主题的时候进行设置

    change_Color(index, color) {
      this.actived = index;
      this.theme.select(color);
      if (color == "yello") {
        this.bgc = "#FFFFF0";
        this.$refs.main.style.color = "black";
      }
      if (color == "white") {
        this.bgc = "#fff";
        this.$refs.main.style.color = "black";
      }
      if (color == "black") {
        this.$refs.main.style.color = "white";
        this.bgc = "#000";
      }
      if (color == "green") {
        this.bgc = "rgba(152, 251, 152, 0.8)";
        this.$refs.main.style.color = "black";
      }
      if (color == "blue") {
        this.bgc = "#E0FFFF";
        this.$refs.main.style.color = "black";
      }
      this.saveset();
      this.show = false;
      this.isShow = false;
    },

当然不要忘记了修改设置选项的背景色和文字的颜色. 然后设置完了对其进行接口请求,将其存入数据库中即可