项目地址: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.urloptions.header = {appid,token}options.method = options.method || 'GET'resolve(options)})},// 响应拦截器 (接收参数请求后得到的数据,处理非成功数据reject,并将数据剥离返回resolve)responseRequest(data) {return new Promise((resolve, reject) => {const [error, res] = dataif (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 = urloptions.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 = urloptions.data = dataoptions.method = 'POST'return this.request(options)},// 文件上传upload(url, data = null, options = {}) {const toast = function(title, icon) {uni.showToast({title,icon})}options.url = urloptions.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不管成功失败都会调用// 停止刷新loadinguni.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.confirmconsole.log(!!e.confirm);}// #ifdef H5this.handleH5WxLogin()// #endif},methods: {mpWxLogin(e) {if (!this.beforeLogin()) {return}let rawData = e.detail.rawDatauni.login({provider: "weixin",success: (res) => {let code = res.codeuni.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 H5tool.getH5Code()// #endif// #ifdef APP-PLUSthis.appWxLogin()// #endif},appWxLogin() {uni.login({provider: "weixin",success: (res) => {let {access_token,openid} = res.authResultuni.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 H5uni.switchTab({url: "../tabbar/home/home"})// #endif// #ifndef H5this.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 = nullexport 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 = 60timer = 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 = 1this.getData().finally(() => {uni.stopPullDownRefresh()})},onReachBottom() {this.handleLoadMore()},methods: {pay(no) {// H5支付// #ifdef H5uni.navigateTo({url: '../h5pay/h5pay?no=' + no,});// #endif// app端/小程序端支付// #ifdef APP-PLUS || MP$tool.wxpay(no, () => {this.page = 1this.getData()})// #endif},handleLoadMore() {if (this.loadStatus != 'more') {return}this.page = this.page + 1this.getData()},getData() {let page = this.pagereturn 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 = userInfostate.token = userInfo.tokenuni.setStorageSync('userInfo', JSON.stringify(userInfo))uni.$emit('userLogin', userInfo)},loginOut({state}, data) {state.userInfo = nullstate.token = nulluni.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.valuethis.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.currentconst 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') returncurrentTab.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.idif(!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 H5uni.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.idif(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 = resconsole.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_coursesconsole.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 = falsethis._audioContext.src = this.src// 播放this._audioContext.onPlay(() => {console.log('开始播放');});// 音频进入可以播放状态this._audioContext.onCanplay(() => {this.duration = this._audioContext.duration})//  音频播放进度更新事件this._audioContext.onTimeUpdate((e) => {if (this.changing) returnthis.currentTime = this._audioContext.currentTime// 获取播放进度,传给父组件if (this.duration > 0) {this.$emit('onProgress', ((this.currentTime / this.duration) * 100).toFixed(2))}});// 播放结束this._audioContext.onEnded(() => {this.currentTime = 0this.isPlaying = falsethis.isPlayEnd = true});// 播放错误this._audioContext.onError(() => {this.isPlaying = false});this.dataStatus =trueconsole.log(this.dataStatus,'加载状态',this.duration,'总时长');},// 播放事件play() {if (!this.src) return this.$toast('数据有误,请联系管理员')if (this.isPlaying) {return this.pause()}this.isPlaying = truethis._audioContext.play()this.isPlayEnd = false},// 暂停事件pause() {console.log('暂停');this.isPlaying = falsethis._audioContext.pause()},stop() {this.isPlaying = falsethis._audioContext.stop()},// 循环播放loop() {this.loopStatus = !this.loopStatuslet toast = this.loopStatus ? '开启循环' : '关闭循环'this._audioContext.loop = this.loopStatusthis.$toast(toast)},// 拖动事件changeSlider(e) {// console.log(e.detail.value,'e');this._audioContext.seek(e.detail.value)this.changing = false},// 拖动中事件changingSlider(e) {this.changing = truethis.isPlaying = falsethis.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.progressconsole.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 H5import 'xgplayer'import FlvPlayer from "xgplayer-flv"// #endifexport 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 H5this.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 H5this.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 H5this.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 : colorrgb.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 H5import 'xgplayer'import FlvPlayer from "xgplayer-flv"// #endifexport 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 H5this.videoContext.off('timeupdate', this.handleTimeUpdate)// #endif},methods: {// #ifdef MPstatechange(e){console.log('live-player code:', e.detail.code)},error(e){console.error('live-player error:', e.detail.errMsg)},// #endifgetData() {this.$api.getLiveComment({page: 1,limit:500,live_id: this.detail.id}).then(res => {// #ifdef H5this.initH5Video(res.rows)// #endif// #ifdef APP-PLUSthis.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 = truethis.$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 H5this.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-PLUSthis.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 : colorrgb.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.typethis.id = e.idthis.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 = resconsole.log(this.item, 'this.item');}).finally(() => {uni.hideLoading()this.getCouponList()})},goCouponList() {if (!this.couponCount) returnthis.$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_idthis.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 H5this.$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 H5this.$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 exceptionif(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-PLUSlet 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 MPlet [err,e] =  await uni.login({provider:"weixin"})if(err){return uni.showModal({content: '支付失败,原因是:'+err.errMsg,showCancel: false,});}let code = e.codelet 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() {// 微信公众号的appidlet appid = 'wxc6491f5743c52eef'let href = window.location.hrefif (href.indexOf('?code') != -1) {let h = href.split('#/')h[0] = window.location.protocol + "//" + window.location.hosthref = 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、打包上线

uniapp商城开发笔记相关推荐

  1. Uniapp商城项目【详细笔记文档】

    文章目录 前言 一.创建项目和引入文件 二.[底部]导航开发 三.[首页]顶部开发 四.[首页]swiper部分 五.[首页]推荐部分开发 六.[首页]文字封装开发 七.[首页]商品列表和单个商品组件 ...

  2. Shopro商城 高级版 Fastadmin和Uniapp进行开发的多平台商城(微信公众号、微信小程序、H5网页、Android-App、IOS-App)

    Shopro商城无加密的开源源码(可用于自营+外包项目(多主体).可用于外包定制开发项目) shopro 商城,一款基于 uni-app 的前端模板商城.目前适配了(小程序+app+h5+公众号). ...

  3. Shopro商城,基于Fastadmin和Uniapp进行开发的多平台(微信公众号、微信小程序、H5网页、Android-App、IOS-App)购物商城

    Shopro商城 基于Fastadmin和Uniapp进行开发的多平台(微信公众号.微信小程序.H5网页.Android-App.IOS-App)购物商城,拥有强大的店铺装修.小程序直播.自定义模板. ...

  4. vue+uni-app商城实战 | 第一篇:从0到1快捷开发一个商城微信小程序,无缝接入OAuth2实现一键授权登录

    一. 前言 本篇通过实战来讲述如何使用uni-app快速进行商城微信小程序的开发以及小程序如何接入后台Spring Cloud微服务. 有来商城 youlai-mall 项目是一套全栈商城系统,技术栈 ...

  5. uniapp小程序商城开发thinkphp6积分商城、团购、秒杀 封装APP

    uniapp小程序商城开发thinkphp6积分商城.团购.秒杀 封装APP,后台是vue开发 需要源代码的可以联系我,找我要哦 <template><view v-if=" ...

  6. 【微信小程序】基于Java+uniapp框架开发的全开源微信小程序商城系统

    应用介绍 基于Java+uniapp框架开发的全开源微信小程序商城系统,前端采用目前主流的uniapp框架开发,后端采用Java语言开发,前后端代码全部开源,减少重复造轮子,支持小程序商城秒杀.优惠券 ...

  7. Vue PC商城项目开发笔记与问题汇总

    Vue PC商城项目开发笔记与问题汇总 负责PC端商城项目,这也是人生第一个真正的项目.刚做了一天,就遇到不少问题,在这里列出自己的问题与解决办法,与大家交流,提升自己,希望以后不会掉进同一个坑里. ...

  8. 基于Django的商城开发项目笔记(一)

    基于Django的商城开发项目笔记(一) 一.环境搭建 1.安装Python:去Python官网下载最新版本Python进行安装,安装时记得勾选将Python加入系统环境变量 2.在命令行输入pip ...

  9. python写微信小程序商城_Python(Django 2.x)+Vue+Uniapp微信小程序商城开发视频教程

    重要的事儿说在前面: 这并非是一个基础课程,请没有相关技术基础知识的同学先学一下基础知识. 本次分享虽然使用Uni-app这个"开发一次,多端覆盖"的框架,但只会给大家分享演示&q ...

最新文章

  1. Transformer又来搞事情!百万像素高清图轻松合成,效果迷人
  2. 怎么用python画花瓣_怎么用python画花朵
  3. 同学遇见过的面试问题
  4. 英雄无敌3高清 Android,安卓TOP10:《英雄无敌3》高清重制版上架
  5. android按键事件响应函数,android 响应按键按下的onKeyDown()函数?
  6. Qt工作笔记-QList (链表) QVector (数组)【转载】
  7. ningx修改mysql数据库密码_windows下面的php+mysql+nginx
  8. ecilpse+python中文输入输出
  9. html 多页面合并,让多个HTML页面 使用 同一段HTML代码
  10. dlib+OpenCV实现人脸登录系统
  11. php选中文本区域,php – 将新行更改为文本区域
  12. oppo r9s 解bl锁,刷入第三方recovery
  13. MYSQL攻击全攻略
  14. 为什么越来越多的人从开发转测试?
  15. Unity (NavMeshAgent 导航系统)
  16. Matlab二元函数图像绘制
  17. 【名企秋招】360公司2017年秋季校招开始喽~ 立即报名
  18. 行业分析:中国企业网盘市场目前现状及未来发展透视
  19. 华为设备在路由引入时应用路由策略
  20. 使用STM32 和 TF卡、VS1003制作MP3

热门文章

  1. 视频直播源码,插入图片、删除图片、设置图片大小、提取图片
  2. 用Python脚本一键自动整理文件,轻松办公
  3. ctrl+/加注释,去注释_关于以下内容的注释:2014年Google I / O上的“绩效文化”
  4. C#数据库教程7-ADO.NET三层架构和数据库DBNull问题
  5. VC智能感知 clw ncb bsc文件的作用
  6. “BT天堂”负责人、“澄空学园”字幕组成员被抓
  7. ffmpeg学习二:《FFmpeg Basics》读书笔记(上)
  8. Diameter在3G中应用的研究
  9. Matlab画图并在图中标记数据点
  10. 计算机基础的英语单词怎么写,计算机基础英语单词