uniapp商城开发笔记

在线体验地址:http://amuniapp.top/

项目地址:https://gitee.com/xiao-ming-1999/uniapp-online-education.git

1、首页配置项pages.json

项目pages.json配置代码

{
  // 主包
  "pages": [{
    "path": "pages/tabbar/index/index",
    "style": {
      "app-plus": {
        // 隐藏导航栏
        "titleNView": false
      },
      // 下拉刷新
      "enablePullDownRefresh": true
    }
  }, {
    "path": "pages/tabbar/learn/learn"
  }, {
    "path": "pages/tabbar/home/home",
    "style": {
      "enablePullDownRefresh": false, // 刷新
      "navigationBarBackgroundColor": "#5ccc84", //导航栏背景色
      "navigationBarTextStyle": "white", // 文字颜色
      "app-plus": {
        "titleNView": { // 自定义导航栏
          "titleAlign": "left",
          "titleText": "我的",
          "buttons": [{ // 自定义按钮
            "type": "menu"
          }]
        }
      }
    }
  },
    {
      "path": "pages/login/login",
      "style": {
        "app-plus": {
          "titleNView": false
        }
      }
    }, {
      "path": "pages/userNeedKnow/userNeedKnow",
      "style": {
        "app-plus": {
          "titleNView": false
        },
        "enablePullDownRefresh": false
      }

    }, {
      "path": "pages/search/search",
      "style": {
        "enablePullDownRefresh": false,
        "app-plus": {
          "titleNView": {
            "searchInput": {
              "placeholder": "请输入关键词搜索",
              "autoFocus": true,
              "align": "left",
              "backgroundColor": "#f8f8f8",
              "borderRadius": "50px"
            },
            "buttons": [{
              "text": "搜索",
              "fontSize": "15px"
            }]
          }
        },
        // 小程序不兼容配置搜索框
        "mp-weixin": {
          "navigationStyle": "custom"
        }
      }

    }, {
      "path": "pages/search-result/search-result",
      "style": {
        "enablePullDownRefresh": false,
        "app-plus": {
          "titleNView": {
            "searchInput": {
              "placeholder": "请输入关键词搜索",
              "disabled": true,
              "align": "left",
              "backgroundColor": "#f8f8f8",
              "borderRadius": "50px"
            }
          }

        }
      }
    }, {
      "path": "pages/list/list",
      "style": {
        "navigationBarTitleText": "列表页",
        "enablePullDownRefresh": true
      }

    },
    {
      "path": "pages/update-password/update-password",
      "style": {
        "navigationBarTitleText": "修改密码",
        "enablePullDownRefresh": false
      }
    },
    {
      "path": "pages/webview/webview",
      "style": {
        "navigationBarTitleText": "",
        "enablePullDownRefresh": false
      }

    }
  ],
  // 分包
  "subPackages": [{
    "root": "pages-book",
    "pages": [{
      "path": "my-book/my-book",
      "style": {
        "navigationBarTitleText": "我的电子书",
        "enablePullDownRefresh": true
      }

    }]
  },
    {
      "root": "pages-media",
      "pages": [{
        "path": "live/live",
        "style": {
          "navigationBarTitleText": "直播详情",
          "enablePullDownRefresh": false
        }
      }, {
        "path": "course/course",
        "style": {
          "navigationBarTitleText": "",
          "enablePullDownRefresh": false
        }

      }, {
        "path": "column/column",
        "style": {
          "navigationBarTitleText": "",
          "enablePullDownRefresh": false
        }

      }]
    },
    {
      "root": "pages-order",
      "pages": [{
        "path": "creat-order/creat-order",
        "style": {
          "navigationBarTitleText": "创建订单",
          "enablePullDownRefresh": false
        }
      }, {
        "path": "h5pay/h5pay",
        "style": {
        "navigationBarTitleText": "微信h5支付",
        "enablePullDownRefresh": false
      }
      },
        {
        "path": "order-list/order-list",
        "style": {
        "navigationBarTitleText": "我的订单",
        "enablePullDownRefresh": true,
        "onReachBottomDistance": 100
      }
      }
        ]
      },
        {
        "root": "pages-test",
        "pages": [{
        "path": "test-list/test-list",
        "style": {
        "navigationBarTitleText": "考试列表",
        "enablePullDownRefresh": true
      }

      }, {
        "path": "test-detail/test-detail",
        "style": {
        "navigationBarTitleText": "开始考试",
        "enablePullDownRefresh": false
      }

      }, {
        "path": "my-test/my-test",
        "style": {
        "navigationBarTitleText": "我的考试",
        "enablePullDownRefresh": true
      }
      }]
      },
        {
        "root": "pages-user",
        "pages": [{
        "path": "setting/setting",
        "style": {
        "navigationBarTitleText": "我的设置",
        "backgroundColor": "#fff",
        "enablePullDownRefresh": false
      }

      }, {
        "path": "my-coupon/my-coupon",
        "style": {
        "navigationBarTitleText": "我的优惠券",
        "enablePullDownRefresh": true
      }

      }, {
        "path": "user-info/user-info",
        "style": {
        "navigationBarTitleText": "编辑资料",
        "enablePullDownRefresh": false
      }

      },
        {
        "path": "bind-phone/bind-phone",
        "style": {
        "app-plus": {
        "titleNView": false
      },
        "enablePullDownRefresh": false
      }
      }, {
        "path": "forget-password/forget-password",
        "style": {
        "app-plus": {
        "titleNView": false
      },
        "enablePullDownRefresh": false
      }
      }
        ]
      }
        ],
        // 分包预载配置
        "preloadRule": {
        "pages-user/my-coupon/my-coupon": {
        "network": "all",
        "packages": ["__APP__"]
      }
      },
        "globalStyle": {
        "navigationBarTextStyle": "black",
        "navigationBarTitleText": "uniApp在线教育",
        "navigationBarBackgroundColor": "#ffffff",
        "backgroundColor": "#ffffff",
        "app-plus": {
        "background": "#efeff4"
      }
      },
        "tabBar": {
        "color": "#BBBAC7",
        "selectedColor": "#2c2c2c",
        "borderStyle": "black",
        "list": [{
        "pagePath": "pages/tabbar/index/index",
        "iconPath": "/static/tabbar/index1.png",
        "selectedIconPath": "/static/tabbar/index1_selected.png",
        "text": "首页"
      },
        {
        "pagePath": "pages/tabbar/learn/learn",
        "iconPath": "/static/tabbar/learn.png",
        "selectedIconPath": "/static/tabbar/learn_selected.png",
        "text": "学习"
      },
        {
        "pagePath": "pages/tabbar/home/home",
        "iconPath": "/static/tabbar/home.png",
        "selectedIconPath": "/static/tabbar/home_selected.png",
        "text": "我的"
      }
        ]
      },
        "condition": { //模式配置,仅开发期间生效
        "current": 0, //当前激活的模式(list 的索引项)
        "list": [{
        "name": "", //模式名称
        "path": "", //启动页面,必选
        "query": "" //启动参数,在页面的onLoad函数里面得到
      }]
      }
      }

2、uni拦截器/接口 封装

1、拦截器封装(get/post/upload)
请求拦截器理解:就是将调用拦截器的方法传进的option参数,统一做一些参数上的添加处理
思路:
uni.request()返回一个promise

一、请求拦截器:

1.给请求添加公共请求头以及baseurl
2.返回一个promise

二、响应拦截器:

1.将错误状态码进行判断,返回一个reject
2.将数据进行剥离,请求数据返回给调用者

三、get/post/upload请求封装:

get接收参数:url、params(拼接在url后面),options

注意点:params应该为对象形式({a:1,b:2}),将对象形式转为a=1&b=2
解决:Object.keys(params).map(key => key + '=' + params[key]).join('&')

upload:接收文件参数(包含文件名,文件路径),调用uni.uploadFile将相应请求头,参数传
递,对相应结果进行判断,并返回给调用接口,并对上传进度进行监听

封装后的代码

import store from "@/store/index.js"
export default {
  // 请求拦截器 原理:利用微任务.then 让所有使用请求拦截器的函数 在拦截器函数之后执行
  config: {
    // 请求拦截器(给请求统一添加 公共请求头、baserUrl)
    beforeRequest(options = {}) {
      return new Promise((resolve, reject) => {
        const baseUrl = 'http://demonuxtapi.dishait.cn'
        const appid = 'bd9d01ecc75dbbaaefce'
        const token = store.state.token
        // 添加公共请求参数
        options.url = baseUrl + options.url
        options.header = {
          appid,
          token
        }
        options.method = options.method || 'GET'
        resolve(options)
      })
    },
    // 响应拦截器 (接收参数请求后得到的数据,处理非成功数据reject,并将数据剥离返回resolve)
    responseRequest(data) {
      return new Promise((resolve, reject) => {
        const [error, res] = data
        if (res.data.msg !== 'ok') {
          const msg = res.data.data || '请求失败'
          uni.showToast({
            title: msg,
            icon: 'none'
          })
          if(msg === 'Token 令牌不合法,请重新登录' || res.data.data === '您没有权限访问该接口!') {
            store.dispatch('loginOut')
            uni.navigateTo({
              url:'/pages/login/login'
            })
          }
          return reject(msg)
        }
        return resolve(res.data.data)
      })
    }
  },
  request(options) {
    // console.log(options, 'options');
    // 使用的beforeRequest方法也是promise应将beforeRequest方法直接返回 (不然读不到return uni.request(opt))
    return this.config.beforeRequest(options).then(opt => {
      return uni.request(opt)
    }).then(this.config.responseRequest)
  },
  get(url, params = null, options = {}) {
    options.url = url
    options.url += params ? ('?' + Object.keys(params).map(key => key + '=' + params[key]).join('&')) : ''
    options.method = 'GET'
    return this.request(options)
  },
  post(url, data = null, options = {}) {
    options.url = url
    options.data = data
    options.method = 'POST'
    return this.request(options)
  },
  // 文件上传
  upload(url, data = null, options = {}) {
    const toast = function(title, icon) {
      uni.showToast({
        title,
        icon
      })
    }
    options.url = url
    options.method = 'POST'
    return this.config.beforeRequest(options).then(opt => {
      return new Promise((resolve, reject) => {
        const uploadTask = uni.uploadFile({
          url: options.url, 
          filePath: data.file,
          name: 'file',
          header: options.header,
          success: (res) => {
            if (res.statusCode !== 200) {
              toast('上传失败', 'fail')
              return reject('上传失败' + errMsg)
            }
            toast('上传成功', 'success')
            return resolve(JSON.parse(res.data))

          },
          fail: (res) => {
            toast('上传失败', 'fail')
            return reject('上传失败' + res.errMsg)
          }
        });
        // 上传进度
        if (options.onProgress && typeof options.onProgress === 'function') {
          uploadTask.onProgressUpdate((res) => {
            options.onProgress(res.progress)
          });
        }

      })
    })
  }
}
2、接口统一调用
// api.js
import api from "./request"

export default {
  // 获取首页数据
  getIndexData() {
    return api.get('mobile/index')
  }
}
// main.js

// 将api挂载在全局
import api from "@/api/api.js"

Vue.prototype.$api =api
其余代码省略、、、

2.接口使用

// index.vue
  <template>
    <view>
      <block v-for="(item,index) in templates" :key="index">
        <!-- 搜索模块 -->
        <f-search-bar v-if="item.type == 'search'" :placeholder="item.placeholder"></f-search-bar>
        <!-- 轮播图模块 -->
        <template v-else-if="item.type == 'swiper'">
          <swiper :indicator-dots="true" :autoplay="true" :interval="3000" :duration="1000"
            class="flex justify-center mt-2">
            <swiper-item class="flex justify-center shadow" v-for="(item,index) in item.data" :key="index">
              <image :src="item.src" mode="aspectFill" style="width: 720rpx;height: 300rpx;" class="rounded">
              </image>
            </swiper-item>
          </swiper>
        </template>
  <!-- icon图标模块 -->
  <icon-nav v-else-if="item.type === 'icons'" :list='item.data'></icon-nav>
  <!-- 优惠券模块 -->
  <coupon-list v-else-if="item.type === 'coupon'"></coupon-list>
  <!-- 拼团模块 -->
  <template v-else-if="item.type === 'promotion'">
    <view class="blank-line" />
    <view class="p-2">
      <text class="font-md font-weight-bold">
        {{item.listType === 'group' ?'拼团' :'秒杀'}}
      </text>
    </view>
    <scroll-view scroll-x="true" class="scroll-row mt-1">
      <course-list v-for="(item,index) in groupList" :key="index" :item="item"></course-list>
    </scroll-view>
  </template>
  <!-- 最新列表模块 -->
  <template v-else-if="item.type === 'list'">
    <view class="blank-line" />
    <view class="p-2 flex justify-between">
      <text class="font-md font-weight-bold">最新列表</text>
      <text class="font-sm text-secondary">查看全部</text>
    </view>
    <view>
      <course-list v-for="(item,index) in item.data" :key="index" :item="item" :type="item.listType">
      </course-list>
    </view>
  </template>
  <!-- 底部模块 -->
  <template v-else-if="item.type === 'imageAd'">
    <view class="blank-line" />
    <image :src="item.data" mode="aspectFill" style="height: 375rpx;width: 100%;"></image>
  </template>
  </block>
    </view>
    </template>

    <script>
    export default {
    data() {
      return {
        groupList: [{
          "group_id": 19,
          "id": 12,
          "title": "unicloud商城全栈开发",
          "cover": "http://demo-mp3.oss-cn-shenzhen.aliyuncs.com/egg-edu-demo/79023e0596c23aff09e6.png",
          "price": "4.00",
          "t_price": "10.00",
          "type": "media",
          "start_time": "2021-04-15T16:00:00.000Z",
          "end_time": "2022-05-16T16:00:00.000Z"
          }],
          // 模板数据
          templates: []
          }
          },
          created() {
          this.getData()
          },
          // 监听下拉刷新
          onPullDownRefresh() {
          this.getData()
          },
          methods: {
          getData() {
          this.$api.getIndexData()
          .then(data => {
          this.templates = data
          // console.log(this.templates, 'this.templates');
          }).finally(res => {
          // .finally不管成功失败都会调用
          // 停止刷新loading
          uni.stopPullDownRefresh();
          })

          }
          }
          }
          </script>

3、登录注册、绑定手机页逻辑

1、登录注册

1.登录token和用户信息存入vuex中

2.将store挂载vue原型上

3.调用uni的setStorage方法进行持久化操作

4.登录后在vuex中的login方法中调用uni.setStorageSync,在vuex中写数据初始化方法init,页面刷新在App钩子中调用init方法,该方法将本地储存中的userInfo数据重新赋值给state.userInfo

5、登录后未绑定手机则跳转至绑定手机页

// login.vue 登录注册页
    <template>
      <view>
        <!-- #ifndef MP -->
        <view class="login-back" @click="back">
          <uni-icons type="arrowleft" size="20" color="#FFFFFF"></uni-icons>
        </view>
        <!-- #endif -->
        <view class="login-bg"></view>

        <view class="login">
          <view class="flex">
            <text class="title">{{ type == 'login' ? '登 录' : '注 册' }}</text>
          </view>
          <view class="login-form">
            <uni-icons type="person"></uni-icons>
            <input type="text" placeholder="请输入用户名" class="rounded font-md" v-model="form.username" />
          </view>
          <view class="login-form">
            <uni-icons type="locked"></uni-icons>
            <input type="text" placeholder="请输入密码" class="rounded font-md" v-model="form.password" />
          </view>
          <view class="login-form" v-if="type == 'reg'">
            <uni-icons type="locked"></uni-icons>
            <input type="text" placeholder="请输入确认密码" class="rounded font-md" v-model="form.repassword" />
          </view>

          <view class="bg-main btn" hover-class="bg-main-hover" @click="submit">{{ type == 'login' ? '登 录' : '注 册' }}
          </view>

          <view class="flex align-center justify-between my-3 font">
            <text class="py-3 text-main" @click="changeType">{{ type == 'login' ? '注册账号' : '去登录' }}</text>
            <text class="py-3 text-light-muted" @click="openForget">忘记密码?</text>
          </view>


          <view class="flex align-center justify-center wechatlogin">
            <!-- #ifndef MP -->
            <uni-icons type="weixin" size="25" color="#5ccc84" @click="wxLogin"></uni-icons>
            <!-- #endif -->
            <!-- #ifdef MP -->
            <button type="default" open-type="getUserInfo" @getuserinfo="mpWxLogin">
              <uni-icons type="weixin" size="25" color="#5ccc84"></uni-icons>
            </button>
            <!-- #endif -->
          </view>


          <checkbox-group v-if="type == 'login'" class="flex align-center justify-center mt-4"
            @change="handleCheckboxChange">
            <label class="text-light-muted">
              <checkbox value="1" color="#7fd49e" style="transform: scale(0.7);" :checked="confirm" /><text class="font"
                                                                                                        @click.stop="userNeed">已阅读并同意用户协议&隐私声明</text>
            </label>
          </checkbox-group>

        </view>

      </view>
    </template>

  <script>
    import tool from '@/common/tool.js';
    export default {
      data() {
        return {
          confirm: false,
          type: "login",
          form: {
            username: "",
            password: "",
            repassword: ""
          }
        }
      },
      onLoad(e) {
        if(e.confirm) {
          this.confirm = !!e.confirm
          console.log(!!e.confirm);
        }
        // #ifdef H5
        this.handleH5WxLogin()
        // #endif
      },
      methods: {
        mpWxLogin(e) {
          if (!this.beforeLogin()) {
            return
          }

          let rawData = e.detail.rawData
          uni.login({
            provider: "weixin",
            success: (res) => {
              let code = res.code
              uni.showLoading({
                title: '登录中...',
                mask: false
              });
              this.$api.wxLogin({
                type: "mp",
                rawData,
                code
              }).then(user => {
                this.handleLoginSuccess(user)
              }).finally(() => {
                uni.hideLoading()
              })
            }
          })
        },
        handleH5WxLogin() {
          let code = tool.getUrlCode("code")
        if (!code) {
        return
        }
        uni.showLoading({
        title: '登录中...',
        mask: false
        });
        this.$api.wxLogin({
        type: "h5",
        code
        }).then(user => {
        this.handleLoginSuccess(user)
        }).finally(() => {
        uni.hideLoading()
        })
        },
        wxLogin() {
        if (!this.beforeLogin()) {
        return
        }
        // #ifdef H5
        tool.getH5Code()
        // #endif
        // #ifdef APP-PLUS
        this.appWxLogin()
        // #endif
        },
        appWxLogin() {
        uni.login({
        provider: "weixin",
        success: (res) => {
        let {
        access_token,
        openid
        } = res.authResult
        uni.showLoading({
        title: '登录中...',
        mask: false
        });
        this.$api.wxLogin({
        type: "app",
        access_token,
        openid
        }).then(user => {
        this.handleLoginSuccess(user)
        }).finally(() => {
        uni.hideLoading()
        })
        }
        })
        },
        openForget() {
        uni.navigateTo({
        url: '/pages-user/forget-password/forget-password',
        });
        },
        handleCheckboxChange(e) {
        this.confirm = !!e.detail.value.length
        },
        back() {
        uni.navigateBack({
        delta: 1
        });
        },
        changeType() {
        this.type = this.type == 'login' ? 'reg' : 'login'
        },
        resetForm() {
        this.form = {
        username: "",
        password: "",
        repassword: ""
        }
        },
        beforeLogin() {
        if (!this.confirm && this.type == 'login') {
        this.$toast('请先阅读并同意用户协议&隐私声明')
        return false
        }
        return true
        },
        handleLoginSuccess(user) {
        this.$toast('登录成功')
        this.$store.dispatch('login', user)
        if (!user.phone) {
        uni.redirectTo({
        url: "/pages/bind-phone/bind-phone"
        })
        return
        }
        setTimeout(() => {
        // #ifdef H5
        uni.switchTab({
        url: "../tabbar/home/home"
        })
        // #endif
        // #ifndef H5
        this.back()
        // #endif
        }, 350)
        },
        submit() {
        if (!this.beforeLogin()) {
        return
        }

        uni.showLoading({
        title: '提交中...',
        mask: false
        });
        let data = Object.assign(this.form, {})

        this.$api[this.type](data).then(user => {
        if (this.type == 'reg') {
        this.$toast('注册成功')
        this.resetForm()
        this.changeType()
        } else {
        this.handleLoginSuccess(user)
        }
        }).finally(() => {
        uni.hideLoading()
        })
        },
        userNeed() {
        uni.navigateTo({
        url: '/pages/userNeedKnow/userNeedKnow'
        })
        }
        }
        }
        </script>
2、绑定手机号页面

2.1发送验证码封装成组件(倒计时功能)

2.2手机号绑定成功后在vuex中写一个更新userInfo的方法,在绑定成功后调用此方法

// code-btn.vue
  <template>
    <view class="code-btn bg-main" hover-class="bg-main-hover" @click="sendCode">
      {{ time > 0 ? (time + 's') : '发送' }}
    </view>
  </template>

  <script>
    let timer = null
    export default {
      name:"code-btn",
      props: {
        phone: {
          type: [Number,String],
          default: ''
        },
      },
      data() {
        return {
          time:0
        };
      },
      methods: {
        sendCode() {
          if(this.time > 0){
            return
          }

          this.$api.getCaptchat({
            phone:this.phone
          }).then(res=>{
            console.log(res);
            if(typeof res == 'number'){
              this.$toast('验证码:'+res)
            } else {
              this.$toast('发送成功')
            }
            this.time = 60
            timer = setInterval(()=>{
              this.time--
              if(this.time <= 0){
                clearInterval(timer)
              }
            },5000)
          })


        }
      },
    }
  </script>

  <style>
    .code-btn{
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      width: 200rpx;
      font-size: 14px;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #FFFFFF;
      border-top-right-radius: 8rpx;
      border-bottom-right-radius: 8rpx;
      z-index: 999;
    }
  </style>
// bind-phone
  <template>
    <view>
      <!-- 顶部返回&背景色 -->
      <view class="login-bg" />
      <!-- #ifndef MP -->
      <view class="py-3 px-4 back-btn" @click="goBack">
        <uni-icons color="#fff" type="back" size="20"></uni-icons>
      </view>
      <!-- #endif -->
      <!-- 登录注册 模块 -->
      <view class="login">
        <view class="flex">
          <text class="title">绑定手机号</text>
        </view>
        <view class="login-form">
          <uni-icons type="person"></uni-icons>
          <input type="text" placeholder="请输入手机号" class="rounded font-md" v-model="form.phone" />
        </view>
        <view class="login-form">
          <uni-icons type="locked"></uni-icons>
          <input type="text" placeholder="验证码" class="rounded font-md" v-model="form.code" />
          <code-btn :phone="form.phone"></code-btn>
        </view>

        <view class="bg-main btn" hover-class="bg-main-hover" @click="submit">绑 定</view>
      </view>
    </view>
  </template>

  <script>
    export default {
      data() {
        return {
          form: {}
        }
      },
      methods: {
        submit() {
          console.log(this.form,'form');
          const data = Object.assign(this.form, {})
          this.$api.getBindPhone(data).then(res => {
            if (res === 'ok') {
              this.$toast('绑定成功')
              this.$store.dispatch('updateUserInfo', {
                phone: data.phone
              })
            }
            uni.navigateBack()
            this.form = {}
          }).catch(err => {
            console.log(err, 'err');
          })

        },
        goBack() {
          uni.navigateBack()
        }
      }
    }
  </script>

4、个人中心模块开发思路

4.1、此模块功能点分为(权限验证、修改密码、缓存计算(tool.js)、清除缓存、编辑资料页面,我的订单列表)

1、权限验证:vue原型上挂载方法authJump验证是否登录和是否绑定手机号

2、修改密码模块:修改密码后点保存调接口后提示,延迟几秒后路由退回,清空本地缓存数据

3、缓存计算、清除缓存:

缓存计算:uni.getStoregeInfo获取缓存信息( tool.js内方法转换kb单位)

清除缓存:循环缓存信息内的keys,将userInfo剔除出来,点击清除循环keys,调用removeStoregeSync遍历删除key

4、编辑资料页面:难点为封装upload接口(uni.chooseImage选择本地图片,uni.showActionSheet底部弹框)

5、订单列表页:

下拉刷新 :pages.json内配置开启刷新,下拉刷新钩子onPullDownRefresh监听,监听后让page=1,并重新获取数据

上拉加载:结合uni-load-more组件使用,status属性的三种状态(见下图),获取数据时对状态进行判断,数据长度小于请求长度status为noMore,等于请求长度则为more,上拉触底onReachBottom钩子触发上拉事件,上拉事件内判断status不为more则reture,否则让page+1,刷新数据

<template>
  <view>
    <view v-for="(item,index) in list" :key="index">
      <uni-card isFull note="true">
        <view>
          <view class="flex font-sm text-muted py-2 justify-between">
            <text>订单时间:{{ item.created_time }}</text>
            <text>订单号:{{ item.no }}</text>
          </view>
          <view class="flex font-md">{{ item.goods }}</view>
          <view class="flex font-md justify-end text-danger font-weight-bold">¥{{ item.price }}</view>
        </view>
        <view slot="actions" class="flex align-center py-2"
          :class="item.status == 'success' ? 'text-success' : ''">
          <view>
            {{ item.status == 'success' ? '交易成功' : '等待支付' }}
          </view>
          <view class="ml-auto">
            <main-button bClass="px-2 font" bStyle="height: 70rpx;" v-if="item.status == 'pendding'"
              @click="pay(item.no)">立即支付</main-button>
          </view>
        </view>
      </uni-card>
      <view class="divider"></view>
    </view>

    <uni-load-more :status="loadStatus"></uni-load-more>

  </view>
</template>

<script>
  import $tool from '@/common/tool.js';
  export default {
    data() {
      return {
        loadStatus: "loading",
        page: 1,
        limit: 5,
        list: []
      }
    },
    created() {
      this.getData()
    },
    onPullDownRefresh() {
      this.page = 1
      this.getData().finally(() => {
        uni.stopPullDownRefresh()
      })
    },
    onReachBottom() {
      this.handleLoadMore()
    },
    methods: {
      pay(no) {
        // H5支付
        // #ifdef H5
        uni.navigateTo({
          url: '../h5pay/h5pay?no=' + no,
        });
        // #endif

        // app端/小程序端支付
        // #ifdef APP-PLUS || MP
        $tool.wxpay(no, () => {
          this.page = 1
          this.getData()
        })
        // #endif
      },
      handleLoadMore() {
        if (this.loadStatus != 'more') {
          return
        }
        this.page = this.page + 1
        this.getData()
      },
      getData() {
        let page = this.page
        return this.$api.getOrderList({
          page: this.page,
          limit: this.limit
        }).then(res => {
          this.list = page == 1 ? res.rows : [...this.list, ...res.rows],
            console.log(this.list, 'list');
          if (res.rows.length < this.limit) {
            this.loadStatus = 'noMore'
          } else if (res.rows.length === this.limit) {
            this.loadStatus = 'more'
          }
        }).catch(err => {
          this.loadStatus = 'more'
          if (page > 1) {
            this.page = this.page - 1
          }
        })
      }
    }
  }
</script>

5、首页开发

1、优惠券模块

1.1:用户领取优惠券,判断是否登录,未登录则跳转至登录页

1.2:退出登录刷新优惠券领取状态:在vuex中登录和退出 分别使用uni.$emit('事件名')进行事件触发

首页发布跨组件事件:created钩子内uni.$on('事件名',函数)事件发布,**页面销毁时使用uni.$off('事件名')注销掉监听的两个事件,**只要退出或登录就重新获取优惠券数据刷新状态,

// store.js
import Vue from "vue"
import Vuex from "vuex"

Vue.use(Vuex)

export default new Vuex.Store({
	state: {
		userInfo: null,
		token: null
	},
	actions: {
		// 持久化数据
		init({
			state
		}, data) {
			const userInfo = uni.getStorageSync('userInfo')
			if (userInfo) {
				state.userInfo = JSON.parse(userInfo)
				state.token = JSON.parse(userInfo).token
			}
		},
		login({
			state
		}, userInfo) {
			state.userInfo = userInfo
			state.token = userInfo.token
			uni.setStorageSync('userInfo', JSON.stringify(userInfo))
			uni.$emit('userLogin', userInfo)
		},
		loginOut({
			state
		}, data) {
			state.userInfo = null
			state.token = null
			uni.removeStorageSync('userInfo')
			uni.$emit('userLoginOut', data)
		},
		updateUserInfo({
			state
		}, values) {
			Object.keys(values).forEach(k => state.userInfo[k] = values[k])
			uni.setStorageSync('userInfo', JSON.stringify(state.userInfo))
		}

	}

})
2、搜索模块

1、pages.json内配置 搜索框及搜索按钮

配置后的搜索输入框

2、页面钩子事件

onsearchinput快捷指令 页面钩子监听配置input内的值,并赋值给当前组件变量this.searchValue

onbutton快捷指令 监听配置按钮事件 触发搜索事件

onNavigationBarSearchInputConfirmed监听表单内回车事件 触发搜索事件

3、添加历史记录规则,每触发一次搜索事件,就更新本地储存的历史记录(如果已经有该历史记录, 则将历史记录置顶,如果该值已经是第一个,则不管)

4、onload钩子内获取历史记录数据

5、空值判断、清除记录提示,

6、触发搜索事件后,进入搜索结果页

<template>
	<view>
		<!-- #ifdef MP -->
		<search-bar v-model="searchValue" @confirm="handleSearchEvent()"></search-bar>
		<!-- #endif -->
		<view class="p-2 flex justify-between align-center"  v-if="list.length">
			<text class="font-md font-weight-bold">历史记录</text>
			<text class="font-sm text-secondary" @click="clearHistory">清除全部</text>
		</view>
		<view class="flex flex-wrap p-2">
			<view v-for="(item,index) in list" :key="index" class="border font-sm mr-2 mb-2 p-2"
				style="border-radius: 4rpx;background-color: #f8f8f8;" @click="goResult(index)">{{item}}</view>
		</view>
	</view>
</template>

<script>
	export default {
		data() {
			return {
				list: [],
				searchValue: ''
			}
		},
		onNavigationBarSearchInputChanged(e) {
			this.searchValue = e.text
		},
		onNavigationBarButtonTap() {
			this.handleSearchEvent(this.searchValue)
		},
		onNavigationBarSearchInputConfirmed() {
			this.handleSearchEvent(this.searchValue)
		},
		onLoad() {
			const list = uni.getStorageSync('searchHistory')
			if (list) {
				this.list = JSON.parse(list)
			}
			console.log(this.list,'list');
		},
		methods: {
			goResult(index) {
				this.$navigateTo(`/pages/search-result/search-result?value=${this.list[index]}`)
			},
			handleSearchEvent(v) {
				if (!v) return this.$toast('请输入关键词')
				const findItem = this.list.findIndex(item => item === v)
				if (findItem !== -1 && findItem !== 0) {
					this.list.splice(findItem, 1)
					this.list.unshift(v)
				} else {
					this.list.unshift(v)
				}
				uni.setStorageSync('searchHistory', JSON.stringify(this.list))
				this.$navigateTo(`/pages/search-result/search-result?value=${v}`)
			},
			clearHistory() {
				uni.showModal({
					content: '是否确认清除历史记录',
					success: (res)=> {
						if (res.confirm) {
							uni.removeStorageSync('searchHistory')
							this.list =[]
						} else if (res.cancel) {

						}
					}
				});
			}
		}
	}
</script>

<style>

</style>
<template>
	<view class=" flex flex-column" style="height: 100%;">
		<tabs :current="current" :tabs="tabs" @change="changeTab"></tabs>
		<!-- 搜索内容 -->
		<!-- swiper组件需要设置固定高度,否则内容无法显示 -->
		<view class="flex-1" style="height: 100vh;" > 
			<swiper :current="current" :duration="1000" style="height: 100%;" @change="changeCurrent">
				<swiper-item v-for="(t,tIndex) in tabs" :key="tIndex">
					<scroll-view @scrolltolower="reachBottom(t)" scroll-y="true"
						style="height: 100%;padding-top: 20rpx;">
						<course-list :item="item" v-for="(item,index) in t.list " :key="index" :type="t.type">
						</course-list>
						<!-- 加载loading -->
						<uni-load-more :status="t.loadStatus"></uni-load-more>
					</scroll-view>
				</swiper-item>
			</swiper>
		</view>
	</view>
</template>

<script>
	export default {
		data() {
			return {
				current: 0,
				tabs: [{
						name: '课程',
						loadStatus: 'more',
						type: 'one',
						list: [],
						page: 1
					},
					{
						name: '专栏',
						loadStatus: 'more',
						type: 'two',
						list: [],
						page: 1
					}
				],
				limit: 10,
				searchValue: ''
			}
		},
		onNavigationBarSearchInputClicked() {
			uni.navigateBack()
		},
		onLoad(e) {
			this.searchValue = e.value
			this.getData()
		},
		methods: {
			getData() {
				const currentTab = this.tabs[this.current]
				const data = {
					keyword: this.searchValue,
					type: this.current == 0 ? 'course' : 'column',
					page: currentTab.page
				}
				currentTab.loadStatus = 'loading'
				this.$api.getSearchList(data).then(res => {
					currentTab.list = currentTab.page === 1 ? res.rows : [...currentTab.list, ...res.rows],
						console.log(currentTab.list, 'currentTab.list');
					if (res.rows.length < this.limit) {
						currentTab.loadStatus = 'noMore'
					} else if (res.rows.length === this.limit) {
						currentTab.loadStatus = 'more'
					}
				})
			},
			changeTab(e) {
				this.current = e
			},
			changeCurrent(e) {
				this.current = e.detail.current
				const currentTab = this.tabs[this.current]
				if (currentTab.loadStatus === 'more' && currentTab.page === 1) {
					this.getData()
				}
			},
			// 滚动底部事件
			reachBottom(t) {
				console.log('触发底部事件');
				const currentTab = this.tabs[this.current]
				if (currentTab.loadStatus !== 'more') return
				currentTab.page++
				this.getData()
			}
		}
	}
</script>

<style scoped lang="less">
	page {
		height: 100%;

		.result {
			height: 100%;
		}
	}
</style>
3、专栏、课程详情页

1、课程详情页:

1、基本思路

uni.setNavigationBarTitle修改标题名

富文本插件:mp-html https://ext.dcloud.net.cn/plugin?id=805 直接导入项目即可

1.1、对购买状态进行判断,根据数据做对应渲染 (购买后顶部图片、简介、购买按钮不显示,将课程简介转为课程内容)

1.2、加载时会出现白色页面一闪而过(骨架屏)

2、视频组件 video

3、音频组件 创建f-audio公共组件

3.1、uni的滑动选择器组件 uslider

3.2、使用uni.createInnerAudioContext创建音频对象 并赋值给data内的_audioContext,在created钩子中对音频对象进行操作,

data中定义:_audioContext存储音频对象,isplaying播放状态,playEnd播放结束状态,currenTime当前时间, duration总时长,isChangeing拖动中状态

3.3、tool.js中转换时分秒方法

3.1、课程详情页(专栏详情页与其类似)
// course.vue
<template>
	<view>
		
		<view class="position-relative">
			<image :src="detail.cover" style="width: 100%;height: 420rpx;" class="bg-light"></image>
			<view class="text-white font-sm p-1" style="position: absolute;right: 20rpx;bottom: 20rpx;background-color: rgba(0,0,0,0.4);">
				专栏
			</view>
		</view>

		<!-- 活动条 -->
		<active-bar v-if="activeData && !detail.isbuy" :end_time="activeData.data.end_time" :price="activeData.data.price" :t_price="detail.price">
			<text v-if="activeData.type == 'group'">{{ activeData.data.p_num }}人拼团</text>
			<text v-else>{{ activeData.data.used_num }}人已枪/剩{{ activeData.data.s_num - activeData.data.used_num}}名</text>
		</active-bar>

		<tabs :tabs="tabs" :current="current" @change="clickTab"></tabs>
		
		<!-- 简介 -->
		<view v-if="current == 0" class="animate__animated animate__fadeIn animate__faster">
			<view v-if="firstLoad" class="flex flex-column p-3">
				<text class="mb-1" style="font-size: 38rpx;">{{ detail.title }}</text>
				<view class="flex align-center justify-between">
					<text class="font-sm text-light-muted">{{ detail.sub_count }} 人学过</text>
				</view>
				<view v-if="!detail.isbuy" class="flex mt-2 align-end">
					
					<text class="text-danger font-lg">¥{{ detail.price }}</text>
					<text class="font-sm text-light-muted ml-1 text-through">¥{{ detail.t_price }}</text>
				</view>
			</view>
			
			<view v-else class="flex flex-column p-3">
				<skeleton width="600rpx" height="75rpx" oClass="mb-2"></skeleton>
				<skeleton width="150rpx" height="70rpx"></skeleton>
				<view class="flex mt-2 align-end">
					<skeleton width="150rpx" height="70rpx"></skeleton>
					<skeleton width="150rpx" height="40rpx" oClass="ml-1"></skeleton>
				</view>
			</view>

			<view class="divider"></view>
			
			<uni-card title="专栏简介" isFull>
				<group-works v-if="!detail.isbuy" ref="groupWorks" @updateData="getData"></group-works>
				
				<mp-html :content="detail.content">
					<view class="flex justify-center py-3 text-muted">
						加载中...
					</view>
				</mp-html>
			</uni-card>
		</view>
		<!-- 目录 -->
		<view v-else class="animate__animated animate__fadeIn animate__faster">
			<view class="p-3">
				<view class="border rounded bg-light text-muted p-2">
					共 {{ list.length }} 节
				</view>
			</view>
			
			<menu-item v-for="(item,index) in list" :key="index" :title="item.title" :index="index" @click="openPlay(item)">
				<view class="flex">
					<text class="border text-danger rounded border-danger font-small px-1 mt-1 mr-1">
						{{ item.type | formatType}}
					</text>
					<text v-if="item.price == 0" class="border text-danger rounded border-danger font-small px-1 mt-1">
						免费试看
					</text>
				</view>
			</menu-item>
			
		</view>
		
		
		<template v-if="!detail.isbuy && firstLoad">
			<view style="height: 75px;"></view>
			<view class="fixed-bottom p-2 border-top bg-white">
				<main-button @click="submit">{{ btn }}</main-button>
			</view>
		</template>
		
		
	</view>
</template>

<script>
	export default {
		filters: {
			formatType(t) {
				let c = {
					media:"图文",
					audio:"音频",
					video:"视频"
				}
				return c[t];
			}
		},
		data() {
			return {
				firstLoad:false,
				current:0,
				tabs:[{
					name:"简介",
				},{
					name:"目录",
				}],
				
				detail:{
					id: 0,
					title: "",
					cover: "",
					try: "",
					price: "",
					t_price: "",
					type: "media",
					sub_count: 0,
					content: "",
					isbuy: false,
					isfava:false
				},
				list:[],
				
				group_id:0,
				// 拼团/秒杀详情
				activeData:null,
				
				flashsale_id:0
			}
		},
		computed:{
			btn(){
				if(this.detail.flashsale){
					return '立即秒杀¥'+this.detail.flashsale.price
				}
				if(this.detail.group){
					return '立即拼团¥'+this.detail.group.price
				}
				if(this.detail.price == 0){
					return '立即学习'
				}
				return  '立即订购¥'+this.detail.price
			}
		},
		onLoad(e) {
			this.detail.id = e.id
			if(!this.detail.id){
				this.$toast('非法参数')
				setTimeout(()=>{
					uni.navigateBack({ delta: 1 });
				},700)
				return
			}
			
			if(e.group_id){
				this.group_id = e.group_id
			}
			
			if(e.flashsale_id){
				this.flashsale_id = e.flashsale_id
			}
		},
		onShow(){
			this.getData()
		},
		methods: {
			submit(){
				// 立即拼团
				if(this.group_id){
					uni.showLoading({
						title: '发起拼团中...',
						mask: true
					})
					
					this.$api.createOrder({
						group_id:this.group_id,
					},'group').then(res=>{
						// H5支付
						// #ifdef H5
						uni.navigateTo({
							url: '../h5pay/h5pay?no='+res.no,
						});
						// #endif
						
						// app端支付
						// #ifdef APP-PLUS || MP
						$tool.wxpay(res.no,()=>{
							this.getData()
						})
						// #endif
					}).catch(err=>{
						console.log(err);
					}).finally(()=>{
						uni.hideLoading()
					})
					
					return
				}
				// 立即学习
				if(this.detail.price == 0){
					uni.showLoading({
						title: '加载中...',
						mask: false
					});
					this.$api.learn({
						goods_id:this.detail.id,
						type:"column"
					}).then(res=>{
						this.getData()
					}).finally(()=>{
						uni.hideLoading()
					})
					return
				}
				// 创建订单
				let type = "column"
				let id = this.detail.id
				
				if(this.detail.flashsale){
					type = 'flashsale'
					id = this.flashsale_id
				}
				console.log('创建订单');
				this.$authJump(`/pages-order/creat-order/creat-order?id=${id}&type=${type}`)
			},
			openPlay(item){
				if(item.price != 0 && !this.detail.isbuy){
					return this.$toast('请先购买该专栏')
				}
				this.$authJump(`/pages-media/course/course?id=${item.id}&column_id=${this.detail.id}`)
			},
			clickTab(index){
				this.current = index
			},
			getData(){
				this.$api.readColumn({
					id:this.detail.id,
					group_id:this.group_id,
					flashsale_id:this.flashsale_id
				}).then(res=>{
					this.detail = res
					console.log(this.detail,'detail');
					if(res.group){
						this.activeData = {
							type:"group",
							data:res.group
						}
						this.$refs.groupWorks.init(this.group_id)
					}
					
					if(res.flashsale){
						this.activeData = {
							type:"flashsale",
							data:res.flashsale
						}
					}
					
					this.list = res.column_courses
					console.log(this.activeData,'activeData');
					uni.setNavigationBarTitle({
						title:this.detail.title
					})
				}).catch(err=>{
					if(err == '该记录不存在'){
						setTimeout(()=>{
							uni.navigateBack({ delta: 1 });
						},700)
					}
				}).finally(()=>{
					this.firstLoad = true
				})
			}
		}
	}
</script>
3.2 、音频组件封装

1、created钩子触发createAudio事件

1.1、事件内将uni.createInnerAudioContext()赋值给this._audioContext,由this._audioContext对音频的各种钩子进行监听操作

1.2、播放进度:slider组件拖拽时,在change事件内要修改播放进度.seek(跳转到指定位置),以及需要监听拖拽中事件,data中定义一个状态记录是否为拖动中changing,如果处于拖动中则changing为true,在播放事件的播放中判断changing值,为true则表明为拖动中则要暂停播放时间

1.3、组件销毁之前钩子内判断要停止播放状态

1.4、循环播放:this._audioContext.loop设为true

<template>
	<view style="background-color: #f5f5f3;" class="pb-4" v-if="dataStatus">
		<view class="" style="padding: 50rpx;padding-bottom: 20rpx;">
			<image :src="list.cover" mode="aspectFilla" style="width: 655rpx;height: 400rpx;background-color: red;">
			</image>
		</view>
		<view class="px-3">
			<slider @changing="changingSlider" @change="changeSlider" :value="position" :max="duration" :block-size="20"
				block-color="#5ccc84" activeColor="#5ccc84" />
			<view class="flex justify-between font" style="color:#5ccc84;">
				<text>{{currentTime | formatTime }}</text>
				<text>{{duration | formatTime}}</text>
			</view>
			<view class="flex justify-center pt-3 icons align-center">
				<text class="iconfont icon-ziyuan11" @click="loop" :style="loopStatus ? 'color: #5ccc84;':''"></text>
				<text class="iconfont " :class="isPlaying ? 'icon-tianchongxing-':'icon-bofang2'" @click="play"></text>
				<text class="iconfont icon-shoucang1" @click="collect"></text>
			</view>
		</view>
	</view>
</template>

<script>
	import tool from "@/common/tool.js"
	export default {
		name: "f-audio",
		props: {
			list: Object,
			src: String,
		},
		computed: {
			position() {
				return this.isPlayEnd ? '0' : this.currentTime
			}
		},
		filters: {
			formatTime(s) {
				if (!s) return "00:00:00"
				return tool.formatSeconds(s);
			}
		},
		data() {
			return {
				_audioContext: null,
				// 播放状态
				isPlaying: false,
				// 播放结束状态
				isPlayEnd: false,
				// 当前时间
				currentTime: 0,
				// 总时长
				duration: 0,
				// 拖动中
				changing: false,
				// 循环状态
				loopStatus: false,
				// 加载状态
				dataStatus:false
			};
		},
		beforeDestroy() {
			if (this._audioContext !== null && this.isPlaying) {
				this.stop()
			}
		},
		created() {
			this.createAudio()
		},

		methods: {
			createAudio() {

				this._audioContext = uni.createInnerAudioContext()
				this._audioContext.autoplay = false
				this._audioContext.src = this.src

				// 播放
				this._audioContext.onPlay(() => {
					console.log('开始播放');
				});
				
				// 音频进入可以播放状态
				this._audioContext.onCanplay(() => {
					this.duration = this._audioContext.duration
				})
				// 	音频播放进度更新事件
				this._audioContext.onTimeUpdate((e) => {
					if (this.changing) return
					this.currentTime = this._audioContext.currentTime
					// 获取播放进度,传给父组件
					if (this.duration > 0) {
						this.$emit('onProgress', ((this.currentTime / this.duration) * 100).toFixed(2))
					}

				});
				// 播放结束
				this._audioContext.onEnded(() => {
					this.currentTime = 0
					this.isPlaying = false
					this.isPlayEnd = true
				});
				// 播放错误
				this._audioContext.onError(() => {
					this.isPlaying = false
				});
				this.dataStatus =true
				console.log(this.dataStatus,'加载状态',this.duration,'总时长');
			},
			// 播放事件
			play() {
				if (!this.src) return this.$toast('数据有误,请联系管理员')
				if (this.isPlaying) {
					return this.pause()
				}
				this.isPlaying = true
				this._audioContext.play()
				this.isPlayEnd = false
			},
			// 暂停事件
			pause() {
				console.log('暂停');
				this.isPlaying = false
				this._audioContext.pause()
			},
			stop() {
				this.isPlaying = false
				this._audioContext.stop()
			},
			// 循环播放
			loop() {
				this.loopStatus = !this.loopStatus
				let toast = this.loopStatus ? '开启循环' : '关闭循环'
				this._audioContext.loop = this.loopStatus
				this.$toast(toast)
			},
			// 拖动事件
			changeSlider(e) {
				// console.log(e.detail.value,'e');
				this._audioContext.seek(e.detail.value)
				this.changing = false
			},
			// 拖动中事件
			changingSlider(e) {
				this.changing = true
				this.isPlaying = false
				this.currentTime = e.detail.value

			},
			collect() {
				this.$toast('暂无此功能')
			}
		},
	}
</script>

<style scoped lang="less">
	.icons {
		text {

			&:first-child,
			&:last-child {
				font-size: 30px;
				color: #ccc;
			}

			&:nth-child(2) {
				font-size: 50px;
				margin: 0 50rpx;
				color: #5ccc84;
			}
		}
	}
</style>

6、学习进度开发

1、图文进度

1、监听滚动钩子onPageScroll(只监听图文类型),拿到滚动距离scrollTop

2、只保存滚动最大值(滚动值大于保存值 且判断是否购买)

3、在mp-html的ready事件内,拿到课程内容的高度

4、获取窗口高度 let windowHeight = uni.getSystemInfoSync().windowHeight

onMediaReady(){
				const Query = uni.createSelectorQuery().in(this)
				Query.select('#media').boundingClientRect(data=>{
					this.mediaHeight = parseInt(data.height)
					this.sumMediaProgress()
				}).exec()
			},
// 计算图文课程学习进度
sumMediaProgress(){
  if(this.mediaHeight > 0){
    this.progress = (((this.scrollTop + windowHeight)/this.mediaHeight)*100).toFixed(2)
    this.progress = this.progress > 100 ? 100 : this.progress
    console.log(this.progress);
  }
},
2、视频类型进度

1、video组件上 @timeupdate监听视频进度 获取当前时间和总时长

2、 当前时长/总时长 *100 =学习进度

3、音频类型进度

1、f-audio组件的onTimeUpdate事件内 获取当前时间和结束时间

2、 当前时长/总时长 *100 =学习进度

7、直播模块(live)

思路笔记:

1、course-list 判断一下让其跳转至live页

2、直播模块没有type值,在获取列表时给live添加个type值

3、课程详情页与live页相似,直接copy

西瓜直播(h5直播)使用
npm install xgplayer
npm install xgplayer-flv --save
// live-play
  <template>
    <view>
      <!-- #ifdef H5 -->
      <view id="video"></view>
      <!-- #endif -->
      <scroll-view scroll-y="true" class="ff0000" :style="'height: '+scrollH+'px;'">
        <view class="font text-danger p-2">
          系统提示:直播内容及互动评论须严格遵守直播规范,严禁传播违法违规、低俗血暴、吸烟酗酒、造谣诈骗等不良有害信息。
        </view>
        <view :id="'live_'+item.id" class="p-2 font" v-for="(item,index) in danmuList" :key="index">
          <text class="text-muted">{{ item.name }}:</text>
          {{ item.content }}
        </view>
      </scroll-view>
      <!-- 点击弹出评论 -->
      <view style="height: 50px;"></view>
      <view style="height: 50px;z-index: 1;" class="fixed-bottom bg-white flex align-center px-3">
        <view class="border rounded flex-1 px-2 py-1 text-light-muted bg-light mr-2" @click="openComment()">说一句吧
        </view>
      </view>
      <!-- 弹窗组件 -->
      <comment-popup ref="comment" @send="sendComment"></comment-popup>

    </view>
  </template>

  <script>
    // #ifdef H5
    import 'xgplayer'
    import FlvPlayer from "xgplayer-flv"
    // #endif
    export default {
      name: "live-play",
      props: {
        detail: Object
      },
      data() {
        return {
          scrollH: 500,
          videoContext: null,
          danmuList: [],
          scrollInto: "",
          currentTime: 0
        };
      },
      mounted() {
        this.getData()
      },
      created() {
        let res = uni.getSystemInfoSync()
        this.scrollH = res.windowHeight - uni.upx2px(420) - 50
      },
      beforeDestroy() {
        // #ifdef H5
        this.videoContext.off('timeupdate', this.handleTimeUpdate)
        // #endif
      },
      methods: {
        getData() {
          this.$api.getLiveComment({
            page: 1,
            limit:500,
            live_id: this.detail.id
          }).then(res => {
            console.log(res,'res');
            // #ifdef H5
            this.initH5Video(res.rows)
            // #endif
          })
        },
        // 创建播放器
        initH5Video(comments = []) {
          // 获取弹幕信息,并封装成FlvPlayer可以接收的结构
          comments = comments.map(item => {
            return {
              duration: 5000,
              id: item.id,
              start: item.time,
              txt: `${item.name}: ${item.content}`,
              style: {
                color: item.color,
                borderRadius: '50px',
                padding: '5px 5px',
                backgroundColor: 'rgba(255, 255, 255, 0.1)'
              }
            }
          })
          this.videoContext = new FlvPlayer({
            id: 'video',
            url:this.detail.playUrl,
            isLive: true,
            playsinline: true,
            width: window.inderWidth,
            height: uni.upx2px(420),
            danmu: {
            panel: true, //弹幕面板
            comments, //弹幕数组
            area: { //弹幕显示区域
            start: 0, //区域顶部到播放器顶部所占播放器高度的比例
            end: 1 //区域底部到播放器顶部所占播放器高度的比例
            },
            closeDefaultBtn: false, //开启此项后不使用默认提供的弹幕开关,默认使用西瓜播放器提供的开关
            defaultOff: false //开启此项后弹幕不会初始化,默认初始化弹幕
            }
            })
            // 监听视频播放时间
            this.videoContext.on('timeupdate', this.handleTimeUpdate)
            },
            handleTimeUpdate(e) {
            this.currentTime = e.currentTime
            },
            openComment() {
            this.$refs.comment.open()
            },
            sendComment(content) {
            if (content == '') {
            return this.$toast("弹幕内容不能为空")
            }
            uni.showLoading({
            title: '发送中...',
            mask: false
            });
            this.$api.sendLiveComment({
            live_id: this.detail.id,
            content,
            time: parseInt(this.currentTime * 1000),
            color: this.getRandomColor()
            }).then(res => {
            this.danmuList.push(res)
            setTimeout(() => {
            this.scrollInto = 'live_' + res.id
            }, 300)

            // 同步弹幕到视频中
            // #ifdef H5
            this.videoContext.danmu.sendComment({
            duration: 5000,
            id: res.id,
            start: res.time,
            txt: `${res.name}: ${res.content}`,
            style: {
            color: res.color,
            borderRadius: '50px',
            padding: '5px 5px',
            backgroundColor: 'rgba(255, 255, 255, 0.1)'
            }
            })
            // #endif
            }).finally(() => {
            uni.hideLoading()
            })
            },
            // 随机颜色
            getRandomColor() {
            const rgb = []
            for (let i = 0; i < 3; ++i) {
            let color = Math.floor(Math.random() * 256).toString(16)
            color = color.length == 1 ? '0' + color : color
            rgb.push(color)
            }
            return '#' + rgb.join('')
            }
            },
            }
            </script>

            <style>

            </style>

新东西:

scroll 组件:scroll-into-view=xx容器id (滚动到xx容器id处)

8、多端兼容

视频播放组件(live-play.vue)

<template>
	<view>
		<!-- #ifdef H5 -->
		<view id="video"></view>
		<!-- #endif -->
		<!-- #ifdef MP -->
		<live-player
		  :src="detail.playUrl"
		  autoplay
		  @statechange="statechange"
		  @error="error"
		  style="width: 750rpx; height: 420rpx;"
		/>
		<!-- #endif -->
		<!-- #ifdef APP-PLUS -->
		<video id="video" v-if="showAppVideo" :src="detail.playUrl" controls autoplay style="width: 750rpx; height: 420rpx;" danmu-btn enable-danmu :danmu-list="appDanmuList"></video>
		<view v-else class="flex align-center justify-center bg-dark" style="width: 750rpx; height: 420rpx;">
			<text class="text-white">加载中...</text>
		</view>
		<!-- #endif -->
		<scroll-view scroll-y="true" class="ff0000" :style="'height: '+scrollH+'px;'">
			<view class="font text-danger p-2">
				系统提示:直播内容及互动评论须严格遵守直播规范,严禁传播违法违规、低俗血暴、吸烟酗酒、造谣诈骗等不良有害信息。
			</view>
			<view :id="'live_'+item.id" class="p-2 font" v-for="(item,index) in danmuList" :key="index">
				<text class="text-muted">{{ item.name }}:</text>
				{{ item.content }}
			</view>
		</scroll-view>
		<!-- 点击弹出评论 -->
		<view style="height: 50px;"></view>
		<view style="height: 50px;z-index: 1;" class="fixed-bottom bg-white flex align-center px-3">
			<view class="border rounded flex-1 px-2 py-1 text-light-muted bg-light mr-2" @click="openComment()">说一句吧
			</view>
		</view>
		<!-- 弹窗组件 -->
		<comment-popup ref="comment" @send="sendComment"></comment-popup>

	</view>
</template>

<script>
	// #ifdef H5
	import 'xgplayer'
	import FlvPlayer from "xgplayer-flv"
	// #endif
	export default {
		name: "live-play",
		props: {
			detail: Object
		},
		data() {
			return {
				scrollH: 500,
				videoContext: null,
				danmuList: [],
				scrollInto: "",
				currentTime: 0,
				
				appDanmuList:[],
				showAppVideo:false
			};
		},
		created() {
			let res = uni.getSystemInfoSync()
			this.scrollH = res.windowHeight - uni.upx2px(420) - 50
			// 获取弹幕列表
			this.getData()
		},
		beforeDestroy() {
			// #ifdef H5
			this.videoContext.off('timeupdate', this.handleTimeUpdate)
			// #endif
		},
		methods: {
			// #ifdef MP
			statechange(e){
				console.log('live-player code:', e.detail.code)
			},
			error(e){
				console.error('live-player error:', e.detail.errMsg)
			},
			// #endif
			getData() {
				this.$api.getLiveComment({
					page: 1,
					limit:500,
					live_id: this.detail.id
				}).then(res => {
					// #ifdef H5
					this.initH5Video(res.rows)
					// #endif
					// #ifdef APP-PLUS
					this.initAppVideo(res.rows)
					// #endif
				})
			},
			initAppVideo(comments = []){
				this.appDanmuList = comments.map(o=>{
					return {
						text:`${o.name}: ${o.content}`,
						color: o.color,
						time:parseInt(o.time/1000),
					}
				})
				this.showAppVideo = true
				this.$nextTick(()=>{
					this.videoContext = uni.createVideoContext("video", this)
				})
			},
			// 创建播放器
			initH5Video(comments = []){
				comments = comments.map(o=>{
					return {  
						duration: 5000,
						id: o.id,
						start: o.time,
						txt: `${o.name}: ${o.content}`,
						style: {
							color: o.color,
							borderRadius: '50px',
							padding: '5px 5px',
							backgroundColor: 'rgba(255, 255, 255, 0.1)'
						}
					}
				})
				console.log(comments);
				this.videoContext = new FlvPlayer({
					  id: 'video',
					  url: this.detail.playUrl,
					  isLive: true,
					  playsinline: true,
					  height: uni.upx2px(420),
					  width: window.innerWidth,
					  danmu: {
					      panel: true, //弹幕面板
					      comments, //弹幕数组
					      area: {  //弹幕显示区域
					        start: 0, //区域顶部到播放器顶部所占播放器高度的比例
					        end: 1 //区域底部到播放器顶部所占播放器高度的比例
					      },
					      closeDefaultBtn: false, //开启此项后不使用默认提供的弹幕开关,默认使用西瓜播放器提供的开关
					      defaultOff: false //开启此项后弹幕不会初始化,默认初始化弹幕
					    }
				  });
				  
				  this.videoContext.on('timeupdate',this.handleTimeUpdate)
			},
			handleTimeUpdate(e) {
				this.currentTime = e.currentTime
			},
			openComment() {
				this.$refs.comment.open()
			},
			sendComment(content) {
				if (content == '') {
					return this.$toast("弹幕内容不能为空")
				}
				uni.showLoading({
					title: '发送中...',
					mask: false
				});
				this.$api.sendLiveComment({
					live_id: this.detail.id,
					content,
					time: parseInt(this.currentTime * 1000),
					color: this.getRandomColor()
				}).then(res => {
					this.danmuList.push(res)
					setTimeout(() => {
						this.scrollInto = 'live_' + res.id
					}, 300)

					// 同步弹幕到视频中
					// #ifdef H5
					this.videoContext.danmu.sendComment({
						duration: 5000,
						id: res.id,
						start: res.time,
						txt: `${res.name}: ${res.content}`,
						style: {
							color: res.color,
							borderRadius: '50px',
							padding: '5px 5px',
							backgroundColor: 'rgba(255, 255, 255, 0.1)'
						}
					})
					// #endif
					// #ifdef APP-PLUS
					this.videoContext.sendDanmu({
						text:`${res.name}: ${res.content}`,
						color: res.color,
					})
					// #endif
				}).finally(() => {
					uni.hideLoading()
				})
			},
			// 随机颜色
			getRandomColor() {
				const rgb = []
				for (let i = 0; i < 3; ++i) {
					let color = Math.floor(Math.random() * 256).toString(16)
					color = color.length == 1 ? '0' + color : color
					rgb.push(color)
				}
				return '#' + rgb.join('')
			}
		},
	}
</script>

搜索页兼容(search.vue)

<template>
	<view>
    <!--     配置的搜索栏不兼容小程序 -->
		<!-- #ifdef MP -->
		<search-bar v-model="searchValue" @confirm="handleSearchEvent()"></search-bar>
		<!-- #endif -->
		<view class="p-2 flex justify-between align-center"  v-if="list.length">
			<text class="font-md font-weight-bold">历史记录</text>
			<text class="font-sm text-secondary" @click="clearHistory">清除全部</text>
		</view>
		<view class="flex flex-wrap p-2">
			<view v-for="(item,index) in list" :key="index" class="border font-sm mr-2 mb-2 p-2"
				style="border-radius: 4rpx;background-color: #f8f8f8;" @click="goResult(index)">{{item}}</view>
		</view>
	</view>
</template>

登录页兼容(login.vue)

<template>
	<view>
    <!-- 返回按钮 -->
		<!-- #ifndef MP -->
		<view class="login-back" @click="back">
			<uni-icons type="arrowleft" size="20" color="#FFFFFF"></uni-icons>
		</view>
		<!-- #endif -->
		<view class="login-bg"></view>

		<view class="login">
			<view class="flex">
				<text class="title">{{ type == 'login' ? '登 录' : '注 册' }}</text>
			</view>
			<view class="login-form">
				<uni-icons type="person"></uni-icons>
				<input type="text" placeholder="请输入用户名" class="rounded font-md" v-model="form.username" />
			</view>
			<view class="login-form">
				<uni-icons type="locked"></uni-icons>
				<input type="text" placeholder="请输入密码" class="rounded font-md" v-model="form.password" />
			</view>
			<view class="login-form" v-if="type == 'reg'">
				<uni-icons type="locked"></uni-icons>
				<input type="text" placeholder="请输入确认密码" class="rounded font-md" v-model="form.repassword" />
			</view>

			<view class="bg-main btn" hover-class="bg-main-hover" @click="submit">{{ type == 'login' ? '登 录' : '注 册' }}
			</view>

			<view class="flex align-center justify-between my-3 font">
				<text class="py-3 text-main" @click="changeType">{{ type == 'login' ? '注册账号' : '去登录' }}</text>
				<text class="py-3 text-light-muted" @click="openForget">忘记密码?</text>
			</view>


			<view class="flex align-center justify-center wechatlogin">
				<!-- #ifndef MP -->
				<uni-icons type="weixin" size="25" color="#5ccc84" @click="wxLogin"></uni-icons>
				<!-- #endif -->
				<!-- #ifdef MP -->
				<button type="default" open-type="getUserInfo" @getuserinfo="mpWxLogin">
					<uni-icons type="weixin" size="25" color="#5ccc84"></uni-icons>
				</button>
				<!-- #endif -->
			</view>


			<checkbox-group v-if="type == 'login'" class="flex align-center justify-center mt-4"
				@change="handleCheckboxChange">
				<label class="text-light-muted">
					<checkbox value="1" color="#7fd49e" style="transform: scale(0.7);" :checked="confirm" /><text class="font"
						@click.stop="userNeed">已阅读并同意用户协议&隐私声明</text>
				</label>
			</checkbox-group>

		</view>

	</view>
</template>

创建订单支付兼容(creat-order.vue)

<template>
	<view>
		<course-list :item="item" type="one" :disable ="true"></course-list>
		<uni-list>
			<uni-list-item title="优惠券" showArrow="true" clickable @click="goCouponList">
				<view slot='footer'>
					<view class="font-sm font-weight-bold">
						{{couponShow}}
					</view>
				</view>
			</uni-list-item>
			<uni-list-item title="支付方式">
				<view slot='footer'>
					<view class="font text-success ">
						微信支付
					</view>
				</view>
			</uni-list-item>
		</uni-list>
		<view style="height: 75px;" />
		<view class="buy">
			<view class=" p-2 border-top fixed-bottom bg-white">
				<main-button v-if="item.price" @click="submit">立即购买{{price}}</main-button>
			</view>
		</view>
	</view>
</template>

<script>
	import $tool from "@/common/tool.js"
	export default {
		computed: {
			couponShow() {
				if (this.coupon_price) {
					return `减${this.coupon_price}元`
				}
				return this.couponCount ? `请选择优惠券 (${this.couponCount}张)` : '暂无优惠券'
			},
			price() {
				let p = ((this.item.price * 1000 - this.coupon_price * 1000) / 1000).toFixed(2)
				return p
			}
		},
		data() {
			return {
				type: '',
				id: 0,
				item: {
					"id": 0,

					"title": "",

					"cover": "",

					"price": 0,

					"type": "video"
				},
				couponCount: 0,
				coupon_price: 0,
				user_coupon_id: 0
			}
		},
		onLoad(e) {
			if (!e.type || !e.id) {
				return this.$toast('参数错误!')
			}
			this.type = e.type
			this.id = e.id
			this.getData()
			uni.$on('chooseCoupon', this.handleChooseCoupon)
		},
		beforeDestroy() {
			uni.$off('chooseCoupon', this.handleChooseCoupon)
		},
		methods: {
			getData() {
				uni.showLoading({
					title: 'loading···'
				})
				this.$api.getGoodsList({
					type: this.type,
					id: this.id
				}).then(res => {
					this.item = res
					console.log(this.item, 'this.item');
				}).finally(() => {
					uni.hideLoading()
					this.getCouponList()
				})
			},
			goCouponList() {
				if (!this.couponCount) return
				this.$authJump(`/pages-user/my-coupon/my-coupon?type=${this.type}&goods_id=${this.id}`)
			},
			getCouponList() {
				uni.showLoading({
					title: 'loading···'
				})
				this.$api.getUsableCoupon({
					type: this.type == 'course' ? 'course' : "column",
					goods_id: this.id
				}).then(res => {
					this.couponCount = res
				}).finally(() => {
					uni.hideLoading()
				})
			},
			handleChooseCoupon({
				user_coupon_id,
				price
			}) {
				this.user_coupon_id = user_coupon_id
				this.coupon_price = price
			},
			submit() {
				uni.showLoading({
					title: '创建订单中···'
				})
				let data ={
					goods_id: this.id,
					type: this.type,
					user_coupon_id: this.user_coupon_id
				}
				let type ='save'
				console.log(data,'data');
				if(this.type === 'flashsale') {
					data ={
						flashsale_id :this.id
					}
					type ='flashsale'
				}
				this.$api.createOrder(data).then(res => {
					// h5支付
					// #ifdef H5
					this.$navigateTo('/pages/h5pay/h5pay?no=' + res.no)
					// #endif

					// app端||小程序 支付
					// #ifdef APP-PLUS || MP
					$tool.wxpay(res.no, () => {
						uni.navigateBack({
							delta: 1
						});
					})
					// #endif
					
				}).finally(() => {
					uni.hideLoading()
				})
			}
		}
	}
</script>

9、微信支付

基本逻辑:

1、创建订单页后会生成一个订单号 ,携带订单号跳转至h5支付页

2.1、支付页首先判断是否在微信环境

2.2、调登录方法 会跳转至微信登录页,获取路径中的 登录凭证code

3、拿到支付相应参数,调用支付接口即可

// 创建订单页 submit为点击创建订单后的回调方法
submit() {
  uni.showLoading({
    title: '创建订单中···'
  })
  this.$api.createOrder({
    goods_id: this.id,
    type: this.type,
    user_coupon_id: this.user_coupon_id
  }).then(res => {
    // h5支付
    // #ifdef H5
    this.$navigateTo('/pages/h5pay/h5pay?no=' + res.no) //res.no 为订单号
    // #endif
  }).finally(() => {
    uni.hideLoading()
  })
}
// h5支付页
  <template>
    <view>
      <view class="text-center my-5">{{ statusOptions[status] }}</view>
    </view>
  </template>

  <script>
    import tool from '@/common/tool.js';
    export default {
      data() {
        return {
          status:"pendding",
          statusOptions:{
            pendding:"支付中...",
            success:"支付成功",
            fail:"支付失败"
          }
        }
      },
      async onLoad(e) {
        // 1、判断是否在微信浏览器中
        if(!tool.isInWechat()){
          uni.showModal({
            content: '请在微信浏览器中打开',
            showCancel: false,
            success: res => {
              if(res.confirm){
                location.href = '/'
              }
            },
          });
        }
        // 3、登录后会生成一个code值 获取路径中的code值
        let code = tool.getUrlCode("code")
        if(!code){
          // 2、没有code值则表示未登录 调用小程序登录方法
          tool.getH5Code()
          return
        }

        // 4、请求支付
        try{
          let orderInfo = await this.$api.wxpay({
            no:e.no,
            code,
            type:"h5"
          })
          console.log(orderInfo);
          // {
          //  appId: "wxf0d98abcc66aab61",
          //  nonceStr: "urN2FLRDvsrJuKiQ",
          //  package: "prepay_id=wx06040120387050015fc250c8479f9d0000",
          //  paySign: "1898E6AFC91C27DFF5677F505FD24058",
          //  signType: "MD5",
          //  timeStamp: "1633464080",
          //  timestamp: "1633464080"
          // }

          // H5支付
          this.wxH5Pay(orderInfo,(s)=>{
            console.log(s);
            this.status = 'fail'
          })
        }catch(err){
          //TODO handle the exception
          if(err.indexOf('code been used') != -1){
            tool.getH5Code()
          } else {
            this.status = 'fail'
            this.$toast(err)
          }
        }
      },
      methods: {
        // 微信h5支付 官方方法
        wxH5Pay(data, callback){
          /**
          data:{
            "appId": "wx2421b1c4370ecxxx",   //公众号ID,由商户传入    
              "timeStamp": "1395712654",   //时间戳,自1970年以来的秒数    
              "nonceStr": "e61463f8efa94090b1f366cccfbbb444",      //随机串    
              "package": "prepay_id=up_wx21201855730335ac86f8c43d1889123400",
              "signType": "RSA",     //微信签名方式:    
              "paySign": "oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg==" //微信签名
              }
              **/
              function onBridgeReady() {
              WeixinJSBridge.invoke('getBrandWCPayRequest',data,(res)=> {
              callback(res)
              })
              }
              if  (typeof WeixinJSBridge == "undefined") {
              if ( document.addEventListener ) {
              document.addEventListener('WeixinJSBridgeReady',  onBridgeReady,  false)
              } else  if  (document.attachEvent) {
              document.attachEvent('WeixinJSBridgeReady',  onBridgeReady);
              document.attachEvent('onWeixinJSBridgeReady',  onBridgeReady)
              }
              } else {
              onBridgeReady()
              }
              },
              }
              }
              </script>
// tool.js 公共方法页
import $api from "@/api/api.js"
export default {
  // 微信支付
  async wxpay(no,success = false,fail = false){
    // app端支付
    // #ifdef APP-PLUS
    let orderInfo = await $api.wxpay({ no, type:"app"})
    console.log(orderInfo);
    uni.requestPayment({
      "provider": "wxpay", 
      "orderInfo": orderInfo,
      success:(res2)=> {
        uni.showToast({
          title: '支付成功',
          icon: 'none'
        });
        if(success && typeof success == 'function'){
          success()
        }
      },
      fail:(err)=> {
        console.log(err);
        if(fail && typeof fail == 'function'){
          fail(err)
        }
        uni.showModal({
          content: '支付失败',
          showCancel:false
        });
      }
    })
    // #endif
    // 小程序支付
    // #ifdef MP
    let [err,e] =  await uni.login({
      provider:"weixin"
    })
    if(err){
      return uni.showModal({
        content: '支付失败,原因是:'+err.errMsg,
        showCancel: false,
      });
    }

    let code = e.code
    let orderInfo = await $api.wxpay({ no, type:"mp", code })
    console.log(orderInfo,'orderInfo');
    uni.requestPayment({
      provider: 'wxpay',
      timeStamp: orderInfo.timeStamp,
      nonceStr: orderInfo.nonceStr,
      package: orderInfo.package,
      signType: orderInfo.signType,
      paySign: orderInfo.paySign,
      success: (res)=> {
        uni.showToast({
          title: '支付成功',
          icon: 'none'
        });
        if(success && typeof success == 'function'){
          success()
        }
      },
      fail: (err)=> {
        uni.showModal({
          content: '支付失败,原因是:'+err.errMsg,
          showCancel: false,
        });
      }
    });
    // #endif
  },
  // 是否在微信浏览器中
  isInWechat(){
    return String(navigator.userAgent.toLowerCase().match(/MicroMessenger/i)) === "micromessenger"
  },
  // 获取路径中的参数
  getUrlCode(name) {
    return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.href) ||
                               [, ''
                               ])[1]
                              .replace(/\+/g, '%20')) || null
  },
  // 微信登录
  getH5Code() {
    // 微信公众号的appid
    let appid = 'wxc6491f5743c52eef'
    let href = window.location.href
    if (href.indexOf('?code') != -1) {
      let h = href.split('#/')
      h[0] = window.location.protocol + "//" + window.location.host
      href = h[0] + '/#/' + h[1]
    }
    let local = encodeURIComponent(href);
    const url =
      `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${local}&response_type=code&scope=snsapi_userinfo&state=STATE&connect_redirect=1#wechat_redirect`;
        location.href = url
    }
}

10、小程序分包配置/子包预加载

主包pages放启动必要页面,分包subPackages放其他页面,包预加载preloadRule

// pages.json页
{
	// 主包
	"pages": [{
			"path": "pages/tabbar/index/index",
			"style": {
				"app-plus": {
					// 隐藏导航栏
					"titleNView": false
				},
				// 下拉刷新
				"enablePullDownRefresh": true
			}
		}, {
			"path": "pages/tabbar/learn/learn"
		}, {
			"path": "pages/tabbar/home/home",
			"style": {
				"enablePullDownRefresh": false, // 刷新
				"navigationBarBackgroundColor": "#5ccc84", //导航栏背景色
				"navigationBarTextStyle": "white", // 文字颜色
				"app-plus": {
					"titleNView": { // 自定义导航栏
						"titleAlign": "left",
						"titleText": "我的",
						"buttons": [{ // 自定义按钮
							"type": "menu"
						}]
					}
				}
			}
		},
		{
			"path": "pages/login/login",
			"style": {
				"app-plus": {
					"titleNView": false
				}
			}
		}, {
			"path": "pages/userNeedKnow/userNeedKnow",
			"style": {
				"app-plus": {
					"titleNView": false
				},
				"enablePullDownRefresh": false
			}

		}, {
			"path": "pages/search/search",
			"style": {
				"enablePullDownRefresh": false,
				"app-plus": {
					"titleNView": {
						"searchInput": {
							"placeholder": "请输入关键词搜索",
							"autoFocus": true,
							"align": "left",
							"backgroundColor": "#f8f8f8",
							"borderRadius": "50px"
						},
						"buttons": [{
							"text": "搜索",
							"fontSize": "15px"
						}]
					}
				},
				// 小程序不兼容配置搜索框
				"mp-weixin": {
					"navigationStyle": "custom"
				}
			}

		}, {
			"path": "pages/search-result/search-result",
			"style": {
				"enablePullDownRefresh": false,
				"app-plus": {
					"titleNView": {
						"searchInput": {
							"placeholder": "请输入关键词搜索",
							"disabled": true,
							"align": "left",
							"backgroundColor": "#f8f8f8",
							"borderRadius": "50px"
						}
					}

				}
			}
		}, {
			"path": "pages/list/list",
			"style": {
				"navigationBarTitleText": "列表页",
				"enablePullDownRefresh": true
			}

		},
		{
			"path": "pages/update-password/update-password",
			"style": {
				"navigationBarTitleText": "修改密码",
				"enablePullDownRefresh": false
			}
		},
		{
			"path": "pages/webview/webview",
			"style": {
				"navigationBarTitleText": "",
				"enablePullDownRefresh": false
			}

		}
	],
	// 分包
	"subPackages": [{
			"root": "pages-book",
			"pages": [{
				"path": "my-book/my-book",
				"style": {
					"navigationBarTitleText": "我的电子书",
					"enablePullDownRefresh": true
				}

			}]
		},
		{
			"root": "pages-media",
			"pages": [{
				"path": "live/live",
				"style": {
					"navigationBarTitleText": "直播详情",
					"enablePullDownRefresh": false
				}
			}, {
				"path": "course/course",
				"style": {
					"navigationBarTitleText": "",
					"enablePullDownRefresh": false
				}

			}, {
				"path": "column/column",
				"style": {
					"navigationBarTitleText": "",
					"enablePullDownRefresh": false
				}

			}]
		},
		{
			"root": "pages-order",
			"pages": [{
					"path": "creat-order/creat-order",
					"style": {
						"navigationBarTitleText": "创建订单",
						"enablePullDownRefresh": false
					}
				}, {
					"path": "h5pay/h5pay",
					"style": {
						"navigationBarTitleText": "微信h5支付",
						"enablePullDownRefresh": false
					}
				},
				{
					"path": "order-list/order-list",
					"style": {
						"navigationBarTitleText": "我的订单",
						"enablePullDownRefresh": true,
						"onReachBottomDistance": 100
					}
				}
			]
		},
		{
			"root": "pages-test",
			"pages": [{
				"path": "test-list/test-list",
				"style": {
					"navigationBarTitleText": "考试列表",
					"enablePullDownRefresh": true
				}

			}, {
				"path": "test-detail/test-detail",
				"style": {
					"navigationBarTitleText": "开始考试",
					"enablePullDownRefresh": false
				}

			}, {
				"path": "my-test/my-test",
				"style": {
					"navigationBarTitleText": "我的考试",
					"enablePullDownRefresh": true
				}
			}]
		},
		{
			"root": "pages-user",
			"pages": [{
					"path": "setting/setting",
					"style": {
						"navigationBarTitleText": "我的设置",
						"backgroundColor": "#fff",
						"enablePullDownRefresh": false
					}

				}, {
					"path": "my-coupon/my-coupon",
					"style": {
						"navigationBarTitleText": "我的优惠券",
						"enablePullDownRefresh": true
					}

				}, {
					"path": "user-info/user-info",
					"style": {
						"navigationBarTitleText": "编辑资料",
						"enablePullDownRefresh": false
					}

				},
				{
					"path": "bind-phone/bind-phone",
					"style": {
						"app-plus": {
							"titleNView": false
						},
						"enablePullDownRefresh": false
					}
				}, {
					"path": "forget-password/forget-password",
					"style": {
						"app-plus": {
							"titleNView": false
						},
						"enablePullDownRefresh": false
					}
				}
			]
		}
	],
	// 分包预载配置
	"preloadRule": {
		"pages-user/my-coupon/my-coupon": {
			"network": "all",
			"packages": ["__APP__"]
		}
	},
	"globalStyle": {
		"navigationBarTextStyle": "black",
		"navigationBarTitleText": "uniApp在线教育",
		"navigationBarBackgroundColor": "#ffffff",
		"backgroundColor": "#ffffff",
		"app-plus": {
			"background": "#efeff4"
		}
	},
	"tabBar": {
		"color": "#BBBAC7",
		"selectedColor": "#2c2c2c",
		"borderStyle": "black",
		"list": [{
				"pagePath": "pages/tabbar/index/index",
				"iconPath": "/static/tabbar/index1.png",
				"selectedIconPath": "/static/tabbar/index1_selected.png",
				"text": "首页"
			},
			{
				"pagePath": "pages/tabbar/learn/learn",
				"iconPath": "/static/tabbar/learn.png",
				"selectedIconPath": "/static/tabbar/learn_selected.png",
				"text": "学习"
			},
			{
				"pagePath": "pages/tabbar/home/home",
				"iconPath": "/static/tabbar/home.png",
				"selectedIconPath": "/static/tabbar/home_selected.png",
				"text": "我的"
			}
		]
	},
	"condition": { //模式配置,仅开发期间生效
		"current": 0, //当前激活的模式(list 的索引项)
		"list": [{
			"name": "", //模式名称
			"path": "", //启动页面,必选
			"query": "" //启动参数,在页面的onLoad函数里面得到
		}]
	}
}

11、打包上线

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值