uni-app介绍
uni-app
是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、H5、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉)等多个平台。
两种搭建uni-app项目的方式
两种搭建项目的方式
- 通过 HBuilderX 可视化界面(https://uniapp.dcloud.io/quickstart?id=_1-%E9%80%9A%E8%BF%87-hbuilderx-%E5%8F%AF%E8%A7%86%E5%8C%96%E7%95%8C%E9%9D%A2)
可视化的方式比较简单,HBuilderX内置相关环境,开箱即用,无需配置nodejs。
开始之前,开发者需先下载安装如下工具:HBuilderX:官方IDE下载地址(http://www.dcloud.io/hbuilderx.html)
2.通过vue-cli命令行(https://uniapp.dcloud.io/quickstart?id=_2-%E9%80%9A%E8%BF%87vue-cli%E5%91%BD%E4%BB%A4%E8%A1%8C)
也可以使用 cli
脚手架,可以通过 vue-cli
创建 uni-app
项目。
使用vue-cli创建uni-app项目
步骤
- 全局安装vue-cli脚手架版本4(如果已经安装``,可以跳过, )。
npm install -g @vue/cli@4
2023-0619测试,如果vue-cli的版本是5,会导致安装失败。
# 查看版本, 注意-V是大写的
vue -V
# 安装包
npm install -g @vue/cli@4
- 执行cli ,用指定模板来创建项目
# vue create 是创建项目
# vue create -p dcloudio/uni-preset-vue 是根据指定的模板dcloudio/uni-preset-vue 来创建
# heima-ugo 是项目名称
vue create -p dcloudio/uni-preset-vue heima-ugo
3.选择默认模版
4.运行项目
npm run dev:%PLATFORM%
%PLATFORM%
可取值如下:
值 | 平台 |
---|---|
h5 | H5 |
mp-alipay | 支付宝小程序 |
mp-baidu | 百度小程序 |
mp-weixin | 微信小程序 |
mp-toutiao | 字节跳动小程序 |
mp-qq | qq 小程序 |
运行小程序
步骤:
- 执行小程序端开发命令
npm run dev:mp-weixin
说明:运行成功之后,会自动在项目根目录下,生成小程序项目代码,如图:
在dist/dev目录下会有一个mp-weixin的文件夹
这就是微信小程序的代码!
2.打开小程序开发者工具,导入上一步生成的小程序项目代码
3.运行效果
总结
- 运行:
npm run dev:mp-weixin
开启小程序开发服务器 - 看效果:导入项目下
dist\dev\mp-weixin
目录到小程序开发者工具 - 在项目src目录修改代码后,会自动重新打包小程序代码,实时查看最新效果
Hbuilderx创建并运行uniapp项目
创建
运行
如果不能主动开启微信开发者工具,可以设置下
uni-app目录结构和重点文件
目录结构
一个uni-app工程,默认包含如下目录及文件:
┌─components # uni-app组件目录
│ └─comp-a.vue # 可复用的a组件
├─pages # 业务页面文件存放的目录
│ ├─index
│ │ └─index.vue # index页面
├─static # 存放应用引用静态资源(如图片、视频等)的目录,注意:静态资源只能存放于此
├─App.vue # 应用配置,用来配置App全局样式以及监听 应用生命周期
├─main.js # Vue初始化入口文件
├─manifest.json # 配置应用名称、appid、logo、版本等打包信息
└─pages.json # 配置页面路由、导航条、选项卡等页面类信息
App.vue
它没有template,可以写公共样式,
小程序中最外层的容器不是body,是page
main.js 入口文件
pages.json 页面配置
manifest.json 配置文件
uniapp开发规范
uni-app代码编写,基本语言包括js、vue、css。以及ts、scss等css预编译器。
在app端,还支持原生渲染的nvue,以及可以编译为kotlin和swift的uts。
DCloud还提供了使用js编写服务器代码的uniCloud云引擎。所以只需掌握js,你可以开发web、Android、iOS、各家小程序以及服务器等全栈应用。
为了实现多端兼容,综合考虑编译速度、运行性能等因素,uni-app 约定了如下开发规范:
- 页面文件遵循 Vue 单文件组件 (SFC) 规范,即每个页面是一个.vue文件
- 组件标签靠近小程序规范,详见uni-app 组件规范
- 接口能力(JS API)靠近小程序规范,但需将前缀 wx、my 等替换为 uni,详见uni-app接口规范
- 数据绑定及事件处理同 Vue.js 规范,同时补充了应用生命周期及页面的生命周期
- 如需兼容app-nvue平台,建议使用flex布局进行开发
开发方式:vue + 原生小程序部分用法
uniapp生命周期
uni-app框架的生命周期结合了vue和微信小程序的生命周期,具体如下:
应用级别:使用小程序的规范(App.vue)
onLanch
https://uniapp.dcloud.net.cn/collocation/App.html#applifecycle
页面级别: 使用小程序的规范
onShow, onLoad
https://uniapp.dcloud.net.cn/tutorial/page.html#lifecycle
组件级别:与vue的组件相同
https://uniapp.dcloud.net.cn/tutorial/page.html#componentlifecycle
created, destoryed
uniappapi
https://uniapp.dcloud.net.cn/api/
原则1:小程序中能用的,改个前缀就可以用。
wx.request → uni.request
原则2:uni的api基本上都支持promise
小程序的api是部分支持promise,还有些不支持的。
https://developers.weixin.qq.com/miniprogram/dev/api/ui/interaction/wx.showToast.html
https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html
uni.request(请求)方法的使用
文档:https://uniapp.dcloud.io/api/request/request?id=request
onLoad() {
uni.request({
url: "https://api-hmugo-web.itheima.net/api/public/v1/home/swiperdata"
}).then(res => {
console.log("结果:", res);
})
}
async onLoad() {
const res = await uni.request({
url: "https://api-hmugo-web.itheima.net/api/public/v1/home/swiperdata"
})
console.log(res)
}
注意:uni.request是异步的方法。如果不传入 success、fail、complete 等 callback 参数,将以 Promise 返回数据
uni.getStorageSync() (获取token)
uni.setStorageSync() (保存token)
uni.showLoading() (提示loading)
uni.hideLoading() 关闭提示loading
uniapp中的组件-自定义组件
使用vue的规范,通过4步: 定义,引入,注册,使用
uniapp中的组件-easycom模式
默认开启的easycom
定义,引入,注册,使用
固定格式: components/组件名/组件名.vue
https://uniapp.dcloud.net.cn/component/#easycom组件规范
直接使用组件: ugo-search
自定义的easycom
如果你的目录结构不符合easycom的要求,则需要自己定义一下
1.自定义esycom
2.正常使用组件
uniapp第三方组件库之uview
uniapp是跨端的,那就要求它对应的组件库也要是跨端的,市面上用的比较多的uview
安装
https://www.uviewui.com/components/install.html#hbuilder-x方式
配置步骤
**#1. 引入uView主JS库**
在项目根目录中的main.js中,引入并使用uView的JS库,注意这两行要放在import Vue之后。
// main.js
import uView from '@/uni_modules/uview-ui'
Vue.use(uView)
#2. 在引入uView的全局SCSS主题文件
在项目根目录的uni.scss中引入此文件。
/* uni.scss */
@import '@/uni_modules/uview-ui/theme.scss';
#3. 引入uView基础样式
注意!
在App.vue中首行的位置引入,注意给style标签加入lang="scss"属性
<style lang="scss">
/* 注意要写在第一行,同时给style标签加入lang="scss"属性 */
@import "@/uni_modules/uview-ui/index.scss";
</style>
#4. 开始使用组件
HbuilderX初始化项目
- 创建空项目
- 安装 插件 scss (HBuilderX中安装插件)
- 运行到小程序模拟器
项目配置-基本配置小程序相关
配置manifest.json
文件
配置mp-weixin
节点,设置appid
"mp-weixin": {
/* 微信小程序特有相关 */
"appid": "wxfb52f2d7b2f6123a",
"setting": {
"urlCheck": false
},
"usingComponents": true
},
配置pages.json文件
- 设置tabBar页面
- 设置整体风格
tabBar页面
-
创建页面
-
设置tabBar配置
配置tabBar:
首页 分类 购物车 我的
pages.json
{
"pages": [
//pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/category/category"
},
{
"path": "pages/cart/cart"
},
{
"path": "pages/profile/profile"
}
],
+ "tabBar": {
+ "color": "#000",
+ "selectedColor": "#ea4451",
+ "backgroundColor": "#fff",
+ "borderStyle": "white",
+ "list": [
+ {
+ "text": "首页",
+ "pagePath": "pages/index/index",
+ "iconPath": "static/tabs/icon_home@3x.png",
+ "selectedIconPath": "static/tabs/icon_home_active@3x.png"
+ },
+ {
+ "text": "分类",
+ "pagePath": "pages/category/category",
+ "iconPath": "static/tabs/icon_category@3x.png",
+ "selectedIconPath": "static/tabs/icon_category_active@3x.png"
+ },
+ {
+ "text": "购物车",
+ "pagePath": "pages/cart/cart",
+ "iconPath": "static/tabs/icon_cart@3x.png",
+ "selectedIconPath": "static/tabs/icon_cart_active@3x.png"
+ },
+ {
+ "text": "我的",
+ "pagePath": "pages/profile/profile",
+ "iconPath": "static/tabs/icon_user@3x.png",
+ "selectedIconPath": "static/tabs/icon_user_active@3x.png"
+ }
]
}
}
注意
- 页面路径中的
index文件名
不能省略 - 本地静态资源必须放到static目录中才能正常访问
- 如果在pages中额外手动添加的pages项,不会像微信开发者工具一样能自动创建对应的页面
设置整体风格
globalStyle(对应小程序的全局window配置)
pages下的页面style(对应小程序的页面配置)
pages.json
"pages": [
//pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "U-go",
"navigationBarBackgroundColor": "#ea4451"
},
pages的整体配置
扩展阅读:配置文档(https://uniapp.dcloud.io/collocation/pages)
分包配置
扩展阅读:https://uniapp.dcloud.io/collocation/pages?id=subpackages
小程序分包加载配置
因小程序有体积和资源加载限制,各家小程序平台提供了分包方式,优化小程序的下载和启动速度。
- 所谓的主包(1个),即放置默认启动页面/TabBar 页面,以及一些所有分包都需用到公共资源/JS 脚本
- 分包(多个)默认不需要加载页面资源
原理分析
在小程序启动时,默认会下载主包并启动主包内页面,当用户进入分包内某个页面时,会把对应分包自动下载下来,下载完成后再进行展示。此时终端界面会有等待提示。
步骤
-
分析页面 → 拆分非tabBar页为单独包(分包)
-
新建分包目录packone,和pages目录同级
- 在
pages.json
中配置subPackages
//pages.json
// 分包
"subPackages": [
{
// 子包的根目录
"root": "packone",
// 子包由哪些页面组成
"pages": [
{
"path": "goods/goods",
"style": {
"navigationBarTitleText": "详情"
}
},
{
"path": "list/list"
},
{
"path": "order/order"
},
{
"path": "auth/auth"
}
]
}
]
4.重新编译
5.微信开发者工具中,项目详情=》基本信息中=〉查看分包大小
总结:
- 微信小程序微信小程序每个分包的大小是2M,总体积一共不能超过20M
subPackages
里的pages的路径是root
下的相对路径,不是全路径。
自定义-搜索组件
步骤
- 在src下新建components目录
- 在该目录下新建ugo-search.vue组件,放入搜索结构和样式
- 导入到首页组件中使用
代码
搜索:components/ugo-search/ugo-search.vue
<template>
<view class="search focused1">
<view class="sinput">
<input type="text" placeholder="搜索" />
<button>取消</button>
</view>
<!-- 搜索状态显示=》下边内容 -->
<view class="scontent" style="display: none">
<div class="title">
搜索历史
<span class="clear"></span>
</div>
<!-- 搜索历史 -->
<div class="history">
<navigator url="/pages/list/index">小米</navigator>
<navigator url="/pages/list/index">智能电视</navigator>
<navigator url="/pages/list/index">小米空气净化器</navigator>
<navigator url="/pages/list/index">西门子洗碗机</navigator>
<navigator url="/pages/list/index">华为手机</navigator>
<navigator url="/pages/list/index">苹果</navigator>
<navigator url="/pages/list/index">锤子</navigator>
</div>
<!-- 结果 -->
<scroll-view scroll-y class="result">
<navigator url="/pages/goods/index">小米</navigator>
<navigator url="/pages/goods/index">小米</navigator>
<navigator url="/pages/goods/index">小米</navigator>
<navigator url="/pages/goods/index">小米</navigator>
<navigator url="/pages/goods/index">小米</navigator>
<navigator url="/pages/goods/index">小米</navigator>
<navigator url="/pages/goods/index">小米</navigator>
<navigator url="/pages/goods/index">小米</navigator>
<navigator url="/pages/goods/index">小米</navigator>
<navigator url="/pages/goods/index">小米</navigator>
<navigator url="/pages/goods/index">小米</navigator>
<navigator url="/pages/goods/index">小米</navigator>
<navigator url="/pages/goods/index">小米</navigator>
<navigator url="/pages/goods/index">小米</navigator>
<navigator url="/pages/goods/index">小米</navigator>
<navigator url="/pages/goods/index">小米</navigator>
<navigator url="/pages/goods/index">小米</navigator>
<navigator url="/pages/goods/index">小米</navigator>
</scroll-view>
</view>
</view>
</template>
<script>
export default {
}
</script>
<style lang="scss" scoped>
// 搜索
.search {
width:750rpx;
display: flex;
flex-direction: column;
.sinput {
box-sizing: border-box;
padding: 20rpx 16rpx;
background: #ff2d4a;
position: relative;
//伪元素
&::after {
position: absolute;
top: 28rpx;
left: 302rpx;
content: "";
width: 44rpx;
height: 44rpx;
line-height: 1;
background-image: url(https://static.botue.com/ugo/images/icon_search%402x.png);
background-size: 32rpx;
background-position: 6rpx center;
background-repeat: no-repeat;
}
input {
background: #fff;
flex: 1;
height: 60rpx;
line-height: 60rpx;
text-align: center;
font-size: 24rpx;
color: #bbb;
border-radius: 5rpx;
}
button {
display: none;
margin-left: 20rpx;
width: 150rpx;
height: 60rpx;
line-height: 60rpx;
text-align: center;
font-size: 24rpx;
border-radius: 5rpx;
background: transparent;
color: #666;
}
}
&.focused {
position: absolute;
width: 100%;
height: 100%;
z-index: 10;
.sinput {
display: flex;
background: #eee;
input {
text-align: left;
padding-left: 60rpx;
}
button {
display: block;
}
&::after {
left: 30rpx;
}
}
}
.scontent {
background: #fff;
position: relative;
flex: 1;
padding: 27rpx;
.title {
font-size: 27rpx;
line-height: 1;
color: #333;
}
.clear {
display: block;
width: 27rpx;
height: 27rpx;
float: right;
background-image: url(http://static.botue.com/ugo/images/clear.png);
background-size: cover;
}
.history {
padding-top: 30rpx;
navigator {
display: inline-block;
line-height: 1;
padding: 15rpx 20rpx 12rpx;
background-color: #ddd;
font-size: 24rpx;
margin-right: 20rpx;
margin-bottom: 15rpx;
color: #333;
}
}
.result {
display: none;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: #fff;
navigator {
line-height: 1;
padding: 20rpx 30rpx;
font-size: 24rpx;
color: #666;
border-bottom: 1px solid #eee;
&:last-child {
border-bottom: none;
}
}
}
}
}
</style>
引入并使用搜索组件
在首页pages/index/index.vue
中引入。由于前面采取了easycom的方式,所以,这里可以直接使用组件,而不需要引入,注册。
<template>
<view class="index">
<!-- 搜索 -->
+ <ugo-search />
<!-- ... -->
</view>
</template>
<script>
export default {
data() {
return {
title: "Hello"
};
},
onLoad() {},
methods: {}
};
</script>
搜索组件-交互功能
步骤
- 设置搜索状态数据:isSearch
- 添加获取焦点focus事件处理状态数据
- 根据是否是搜索状态添加搜索样式=>.focused和控制.scontent是否显示
<view class="search"
+ :class="{focused:isSearch}"
>
<view class="sinput">
+ <input @focus="search" type="text" placeholder="搜索" />
+ <button @click="cancel">取消</button>
</view>
<view class="scontent"
+ v-show="isSearch"
>
<div class="title">
搜索历史
<span class="clear"></span>
</div>
<div class="history">
<navigator url="/pages/list/index">小米</navigator>
<navigator url="/pages/list/index">智能电视</navigator>
<navigator url="/pages/list/index">小米空气净化器</navigator>
<navigator url="/pages/list/index">西门子洗碗机</navigator>
<navigator url="/pages/list/index">华为手机</navigator>
<navigator url="/pages/list/index">苹果</navigator>
<navigator url="/pages/list/index">锤子</navigator>
</div>
</view>
</view>
<script>
export default {
data() {
return {
// 是否是搜索状态
+ isSearch: false
};
},
methods: {
search() {
+ this.isSearch = true;
},
cancel() {
+ this.isSearch = false;
}
}
};
</script>
封装请求模块
对uni.request进行封装。
要求:整体封装一个函数
- 入参是:{ url, header = {}, method, data }
- 请求之前有loading
- 请求结束取消loading
- 自动添加token(从storage中取出来, 添加到headers中)
- 返回结果的格式是 {msg, data}
格式如下:
const res = await 封装过的发请求的函数({ 不带基地址的url, header = {}, method, data })
涉及知识
uni.getStorageSync
uni.showLoading()
uni.hideLoading()
步骤
- 新建utils目录
- utils目录下新建request.js模块,定义BASE_URL
- 封装异步函数,接收{ url, method, data }参数,返回结果:msg和data
- 请求前后添加loading效果
- 添加token
代码
request方法的封装
utils/request.js
const baseURL = "https://api-hmugo-web.itheima.net/api/public/v1"
async function request({
url, header={}, method='get', data={}
}) {
uni.showLoading({title:'加载中'})
let token = uni.getStorageSync('token')
if(token) {
header.Authorization = token
};
const res = await uni.request({
url: baseURL + url,
header,
method,
data
})
uni.hideLoading()
if (res.data.meta.status === 200) {
return {
msg: res.data.meta.msg,
data: res.data.message
}
} else {
return Promise.reject(res.data.meta.msg)
}
}
export default request
测试
import request from '@/utils/request.js'
async getSwiper() {
const res = await request({
url: "/home/swiperdata"
});
console.log(res)
},
(1)封装请求模块-挂载到vue实例上
utils/request.js中挂载
++ import Vue from 'vue'
......
++ Vue.prototype.request = request
export default request
main.js中引入
import '@/utils/request.js'
(2)封装请求模块-vue 插件形式(优化)
request.js
// vue 插件形式
// 1. 定义插件
const MyRequest = {
install(Vue, opts) {
Vue.prototype.request = request
}
}
export default MyRequest
main.js
import MyRequest from '@/utils/request';
// 入口文件
// 2. 注册插件
Vue.use(MyRequest)
使用
this.request(config)
config
是请求的配置项,例如请求URL、请求方式、请求头、请求数据
navigator组件(跳转)
**例如:**首页-数据渲染
navigator组件(跳转): https://uniapp.dcloud.io/component/navigator
处理路由跳转url
- 轮播图:‘/packone/goods/index?id=’+item.goods_id
- 功能导航:‘/packone/list/index?query=’+item.name
- 栏目楼层:‘/packone/list/index?query=’ +prd.name
循环渲染
<!-- 轮播图 -->
<view class="swiper">
<swiper
autoplay
interval="2000"
circular
indicator-dots
indicator-color="rgba(255,255,255,1)"
indicator-active-color="rgba(255,255,255,.6)"
>
<swiper-item v-for="item in swiper" :key="item.goods_id">
<navigator :url="'/packone/goods/index?id=' + item.goods_id">
<image :src="item.image_src" />
</navigator>
</swiper-item>
</swiper>
</view>
<!-- 功能导航 -->
<view class="navs">
<navigator
+ :open-type="item.open_type ? 'switchTab' : 'navigate'"
+ :url="
+ item.open_type
+ ? '/pages/category/index'
+ : '/packone/list/index?query=' + item.name
+ "
//item.open_type就跳转到分类页面,没有就跳转到list页面(只有第一个有)
v-for="item in navs"
:key="item.name"
>
<image :src="item.image_src" />
</navigator>
</view>
<!-- 栏目楼层 -->
<view class="floors">
<!-- 1 -->
<view class="floor" v-for="(item, i) in floors" :key="i">
<!-- title -->
<view class="ftitle">
<image :src="item.floor_title.image_src" />
</view>
<!-- pics -->
<view class="fitem">
<navigator
:url="'/packone/list/index?query=' + prd.name"
v-for="prd in item.product_list"
:key="prd.name"
>
<image :src="prd.image_src" />
</navigator>
</view>
</view>
</view>
下拉刷新-配置
- 在
pages.json
文件中pages字段:配置首页的下拉刷新效果 - style(window)属性中:
"enablePullDownRefresh": true
{
"path": "pages/index/index",
"style": {
+ "enablePullDownRefresh": true, // 允许下拉
"backgroundColor": "#fd1800" // 下拉区域背景色
}
},
开启配置:enablePullDownRefresh: https://uniapp.dcloud.io/collocation/pages?id=style
终止状态:uni.stoppulldownrefresh() https://uniapp.dcloud.io/api/ui/pulldown?id=stoppulldownrefresh
下拉刷新-实现
- 调用onPullDownRefresh钩子函数,刷新首页
- 等到首页获取完数据刷新后,关闭下拉loading
onPullDownRefresh() {
Promise.all([this.getSwiper(), this.getNavs(), this.getFloors()]).then(
() => {
// 执行完停止loading
uni.stopPullDownRefresh();
}
)
},
注意:
- 模拟器中:用户手动触发,会自动关闭loading效果
- 真机测试:需要调用uni.stopPullDownRefresh()方法关闭
回到顶部
监听页面滚动:
- 如果 滚动高度 > 半屏高度 → 显示回顶按钮
- 否则,隐藏回顶按钮
涉及知识
- 监听页面滚动条 onPageScroll
https://uniapp.dcloud.io/collocation/frame/lifecycle?id=page - 垂直方向滚动到指定位置
https://uniapp.dcloud.io/api/ui/scroll?id=pagescrollto
uni.pageScrollTo({
scrollTop: 0,
duration: 300
});
实现步骤
- 补充状态 isTop
- onPageScroll绑定点击事件,控制到一定位置显示到顶部按钮
-
- 获取页面半屏高度:uni.getSystemInfoSync().windowHeight / 2
- 如果滚动高度大于半屏高度 ===> 显示回顶按钮
- 调用页面滚动方法回到顶部
uni.pageScrollTo()
核心代码
补充状态
data() {
return {
// 省略其他...
+ isTop: false
}
},
监听滚动事件
onPageScroll(e) {
// console.log('页面滚动:', e)
this.scrollTop = e.scrollTop
// 显示回顶按钮条件:滚动高度大于半屏幕高度
if (this.scrollTop > uni.getSystemInfoSync().windowHeight / 2) {
this.isTop = true
} else {
this.isTop = false
}
}
控制视图显示隐藏
<view v-if="isTop" @click="goTop" class="goTop icon-top"></view>
icon-top是额外定义的类名,用来显示图标。
补充method
methods: {
// 省略其他...
goTop() {
uni.pageScrollTo({
scrollTop: 0,
duration: 300
})
}
}
分类页面
分类-渲染数据(三层嵌套结构)
后端数据有三层嵌套结构,分别对应视图上的三级内容。
左侧一级分类数据,直接循环渲染
右侧二级分类数据,需要根据当前选中的一级分类来确定
实现步骤
- 补充状态值 active,用来表示一级分类选中的下标,默认为0
- 补充计算属性sub,用来计算得出2、3级分类
核心代码
data() {
return {
// 分类数据
cates: [],
active: 0 // 一级分类选中的索引
};
},
computed: {
// 2、3级分类
sub() {
// 默认一级分类的第一项被选中
//
return this.cates.length ? this.cates[this.active].children : [];
}
},
methods: {
// 获取分类数据
async getCate() {
const {data } = await this.request({
url: "/categories"
});
this.cates = data;
}
},
onLoad() {
this.getCate();
}
页面渲染
<!-- 分类 -->
<view class="category">
<!-- 顶级分类 -->
<view class="sup">
<scroll-view scroll-y>
<text
:key="item.cat_id"
v-for="(item,i) in cates"
>{{item.cat_name}}</text>
</scroll-view>
</view>
<!-- 子级分类 -->
<view class="sub">
<scroll-view scroll-y>
<!-- 封面图 -->
<image src="http://static.botue.com/ugo/uploads/category.png" class="thumb" />
<view class="children" :key="item.cat_id" v-for="item in sub">
<view class="title">{{item.cat_name}}</view>
<!-- 品牌 -->
<view class="brands">
<navigator
:url="'/packone/list/index?query='+it.cat_name"
:key="it.cat_id"
v-for="it in item.children"
>
<image :src="it.cat_icon" />
<text>{{it.cat_name}}</text>
</navigator>
</view>
</view>
</scroll-view>
</view>
</view>
计算属性
computed: {
sub() {
return this.list.length ? this.list[this.active].children:[]
}
}
分类-一级分类切换
实现分类切换功能。点击某一项:
- 高亮显示
- 右侧的显示内容联动
实现步骤
- 绑定事件,获取索引,设置当前选中的一级分类索引
- 根据索引添加class高亮样式
<!-- 顶级分类 -->
<view class="sup">
<scroll-view scroll-y>
<text
+ @click="switchCate(i)"
+ :class="{active:i === active}"
:key="item.cat_id"
v-for="(item,i) in cates"
>{{item.cat_name}}</text>
</scroll-view>
</view>
补充切换方法
methods: {
// ...
switchCate(index) {
this.active = index;
}
}
骨架屏
easy-com组件规范
https://uniapp.dcloud.net.cn/component/#easycom组件规范
<template>
<view>
<view :style="{width: width + 'rpx', height: height + 'rpx'}" v-show="!isDone" class="mask"></view>
<image :src= "src" :style="{width: width + 'rpx', height: height + 'rpx'}" @load="isDone=true"/>
</view>
</template>
<script>
export default {
name:"ugo-image",
props: {
src: {type: String, required: true},
width: { type: Number, default: 120 },
height: { type: Number, default: 120 },
},
data() {
return {
isDone: false
};
}
}
</script>
<style>
.mask {
background-color: rgba(0, 0, 0, 0.5);
position: relative;
overflow: hidden;
display: block;
}
.mask::before {
content: '';
position: absolute;
animation: shan 1.5s ease 0s infinite;
top: 0;
width: 50%;
height: 100%;
background: linear-gradient(
to left,
rgba(255, 255, 255, 0) 0,
rgba(255, 255, 255, 0.3) 50%,
rgba(255, 255, 255, 0) 100%
);
transform: skewX(-45deg);
}
@keyframes shan {
0% {
left: -100%;
}
100% {
left: 120%;
}
}
</style>
uniapp的样式不要用scoped
<template>
<view class="test" v-show="isShow">
isShow
<button @click="isShow=false">close</button>
</view>
</template>
<script>
export default {
name:"ugo-test",
data() {
return {
isShow: true
};
}
}
</script>
<style>
.test {
display: block;
}
</style>
搜索-建议商品
实现步骤
- 搜索框绑定input事件和v-model
- 根据关键词查询API接口,获取建议的商品数据,并展示
根据result长度,条件渲染 --> 是否显示建议商品列表
核心代码
search.vue
结构
<view class="sinput">
<input
@focus="search"
type="text"
+ @input="searchPrd"
+ v-model="keyWord"
placeholder="搜索"
/>
<button @click="cancel">取消</button>
</view>
<!-- 搜索状态显示=》下边内容 -->
<view class="scontent" v-show="isSearch">
<!-- 搜索历史 -->
+ <block v-if="result.length === 0">
<view class="title">
搜索历史
<span class="clear"></span>
</view>
<!-- 搜索历史 -->
<view class="history">
<navigator url="/pages/list/list">小米</navigator>
<navigator url="/pages/list/list">智能电视</navigator>
<navigator url="/pages/list/list">小米空气净化器</navigator>
<navigator url="/pages/list/list">西门子洗碗机</navigator>
<navigator url="/pages/list/list">华为手机</navigator>
<navigator url="/pages/list/list">苹果</navigator>
<navigator url="/pages/list/list">锤子</navigator>
</view>
+ </block>
<!-- 搜索建议商品 -->
+ <scroll-view scroll-y class="result" v-else>
<navigator
+ v-for="item in result"
+ :key="item.goods_id"
+ :url="'/packageone/goods/goods?id=' + item.goods_id"
>{{ item.goods_name }}</navigator
>
</scroll-view>
</view>
状态值
data() {
return {
isSearch: false,
+ keyword: "",
+ result: []
};
},
方法
// 获取搜索建议商品 --> 函数防抖处理
async searchPrd () {
// 如果关键词为空=》清除历史建议商品列表
if (!this.keyWord) {
return this.result = []
}
const { data } = await this.request({
url: "/goods/qsearch",
data: {
query: this.keyword
// cid: this.activeId
}
})
this.result = data
}
对搜索功能进行防抖处理
防抖节流,参考: https://juejin.cn/post/7049583495936475150, 代码
防抖:持续触发不执行,不触发一段时间之后,才执行
节流:持续触发也执行,只是执行的频率变低了
对输入框的防抖处理
// 获取搜索建议商品 --> 函数防抖处理
searchPrd () {
this.timer && clearTimeout(this.timer)
this.timer = setTimeout(async () => {
// 如果关键词为空=》清除历史建议商品列表
if (!this.keyword) {
return this.result = []
}
const { msg, data } = await this.request({
url: "/goods/qsearch",
data: {
query: this.keyword
// cid: this.activeId
}
})
this.result = data
}, 600)
}
搜索-跳转到结果页(跳转传参)
input组件上有confirm事件, 直接添加事件监听即可。
在回调函数中,通过navigateTo进行跳转即可
实现步骤
- input绑定confirm事件(测试时,使用回车确认)
- 搜索确认时,路由跳转到结果页并携带查询参数
代码
search.vue
在模板中补充事件监听
<input
+ @confirm="goResult"
@input="searchPrd"
@focus="search"
v-model="keyWord"
type="text"
placeholder="搜索"
/>
设置软键盘上的右下角按钮的文字!!!
confirm-type=“search”
https://uniapp.dcloud.net.cn/component/input.html#confirm-type
在代码中
goResult() {
uni.navigateTo({
url: "/packone/list/index?query="+this.keyword
});
},
搜索-保存和清除历史搜索记录(本地保存)
实现步骤
- 补充状态值:history, 初值为[],用来记录搜索历史数据。它的默认值从本地获取
- 搜索确认时,把搜索关键词保存到本地
- 点击清除按钮,清除本地搜索历史数据
<!-- 搜索历史 -->
<block v-if="result.length === 0">
<div class="title">
搜索历史
+ <span @click="clearHistory" class="clear"></span>
</div>
<!-- 搜索历史 -->
<div class="history">
<navigator
:key="i"
v-for="(item, i) in history"
:url="`/packageone/list/list?query=${item}`"
>{{ item }}</navigator>
</div>
</block>
状态值
data() {
return {
// 省略其他
history: uni.getStorageSync("history") || []
}
}
clearHistory() {
this.history = [];
uni.removeStorageSync("history");
},
goResult () {
// 1. 处理搜索历史
this.history.push(this.keyword)
// 去重
this.history = [...new Set(this.history)]
uni.setStorage({
key: "history",
data: this.history
})
// 2. 跳转
uni.navigateTo({
url: "/packageone/list/list?query=" + this.keyword
})
},
注意
- 设置history默认值,获取本地数据使用同步方法(传入字符串参数)
- 记录要去重
分类-搜索-结果页
实现步骤
- 通过onLoad生命周期获取查询参数
- 调用接口获取结果数据渲染
基础模板
<view>
<!-- 筛选 -->
<view class="filter">
<text class="active">综合</text>
<text>销量</text>
<text>价格</text>
</view>
<scroll-view class="goods" scroll-y>
<!-- 遍历 -->
<view
class="item"
v-for="item in 10"
:key="item">
<!-- 商品图片 -->
<image class="pic" src="http://image5.suning.cn/uimg/b2c/newcatentries/0070166234-000000000630980467_1_400x400.jpg" />
<!-- 商品信息 -->
<view class="meta">
<view class="name"> item.goods_name </view>
<view class="price">
<text>¥</text> item.goods_price
<text>.00</text>
</view>
</view>
</view>
</scroll-view>
</view>
<style lang="scss">
.filter {
display: flex;
height: 96rpx;
line-height: 96rpx;
border-bottom: 1rpx solid #ddd;
/* #ifdef H5 */
position: relative;
z-index: 99;
/* #endif */
text {
flex: 1;
text-align: center;
font-size: 30rpx;
color: #333;
&.active {
color: #ea4451;
}
}
}
.goods {
position: absolute;
width: 100%;
top: 97rpx;
bottom: 0;
}
.item {
display: flex;
padding: 30rpx 20rpx 30rpx 0;
margin-left: 20rpx;
border-bottom: 1rpx solid #eee;
&:last-child {
border-bottom: none;
}
.pic {
width: 200rpx;
height: 200rpx;
margin-right: 30rpx;
}
.meta {
flex: 1;
font-size: 27rpx;
color: #333;
position: relative;
}
.name {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.price {
position: absolute;
bottom: 0;
color: #ea4451;
font-size: 33rpx;
text {
font-size: 22rpx;
}
}
}
</style>
核心代码
补充状态
data() {
return {
+ list: [], // 查询数据
+ total:0 // 总数据条数
};
},
方法
// methods
async getList(data) {
const { msg, data: _d } = await this.request({
url: "goods/search",
data
});
this.total = _d.total
if (msg.status === 200) {
this.list.push(..._d.goods)
}
}
在onLoad中调用
// event
onLoad(params) {
this.getList(params);
}
页面渲染
<!-- 商品列表 -->
<scroll-view class="goods" scroll-y>
<!-- 遍历 -->
<view
class="item"
v-for="item in list"
:key="item.goods_id"
@click="goDetail(item.goods_id)">
<!-- 商品图片 -->
<image class="pic" :src="item.goods_small_logo" />
<!-- 商品信息 -->
<view class="meta">
<view class="name">{{ item.goods_name }}</view>
<view class="price">
<text>¥</text>{{ item.goods_price }}
<text>.00</text>
</view>
</view>
</view>
</scroll-view>
跳转
goDetail(id) {
uni.navigateTo({
url: "/packone/goods/goods?id=" + id
})
}
分类-搜索-结果页-(上拉加载)
分析
-
后台接口有多页的支持
-
检测是否到底
两种方式实现 -
- 对于某个页面:用户滚动到页面底部会触发 onReachBottom事件。(钩子函数)
- 对于某个scroll-view组件:用户滚动到区域底部会触发scrolltolower事件
实现步骤
- 补充查询参数queryData:{query:‘’, pagenum: 1},来记录当前页码和查询关键字
- 在onLoad钩子中,保存查询关键字到queryData中的query
- 给scroll-view 添加scrolltolower事件监听,在回调中,判断是否全部加载结束,如果没有,就将pagenum+1,然后重发请求。
- 请求的数据要以追加的格式保存
代码
<scroll-view @scrolltolower="getMore" class="goods" scroll-y>
状态
data () {
// 存储渲染相关数据
return {
// 列表
list: [],
+ queryData: {
+ pagenum: 1,
+ query: ''
+ },
total: 0
}
},
onLoad(params) {
this.queryData.query = params.query
this.getList();
},
// methods
async getList() {
const {data} = await this.$request({
url: "goods/search",
data: this.queryData
});
this.total =data.total
this.list.push(...data.goods)
},
// 加载更多
getMore () {
if ( this.total === this.list.length ) return
this.queryData.pagenum++
this.getList()
},
补充视图
<scroll-view @scrolltolower="getMore" class="goods" scroll-y>
<!-- 省略其他... -->
<!-- 列表加载完成显示 -->
<view class="nomore" v-if="this.total > 0 && (this.total === this.list.length)">没有更多数据...</view>
</scroll-view>
购物车
商品详情-获取数据渲染
根据获取到的页面参数获取对应商品详情数据并渲染
商品详情-链接跳转
给商品列表页商品添加链接和ID参数
/packageone/list/list.vue
goDetail(id) {
uni.navigateTo({
url: `/packageone/goods/goods?id=${id}`
}
},
代码
补充状态值
/packone/goods/index.vue
data() {
return {
+ goods: null, // 商品详情数据
}
},
补充methods
// methods
async getGoods(goods_id) {
const { data } = await this.request({
url: "/goods/detail",
data: { goods_id }
});
this.goods = data;
}
在钩子函数中调用
onLoad ({id}) {
if(id) this.getGoods(id)
},
页面渲染
<!-- 商品图片 -->
<swiper
v-if="goods.pics"
class="pics"
indicator-dots
indicator-color="rgba(255, 255, 255, 0.6)"
indicator-active-color="#fff"
>
<swiper-item v-for="item in goods.pics" :key="item.pics_id">
<image class="pics" :src="item.pics_mid" />
</swiper-item>
</swiper>
<!-- 基本信息 -->
<view class="meta">
<view class="price">¥{{goods.goods_price}}</view>
<view class="name">{{goods.goods_name}}</view>
<view class="shipment">快递: 免运费</view>
<text class="collect icon-star">收藏</text>
</view>
<!-- 商品详情 -->
<view class="detail">
<view v-html="goods.goods_introduce"></view>
<!-- <rich-text :nodes="goods.goods_introduce"></rich-text> -->
</view>
注意
使用vue指令v-html处理节点字符串
商品详情-微信button打电话-客服
利用微信的button组件实现客服功能
<button open-type="contact" class="icon-handset">联系客服</button>
注意:设置buttion的open-type=“contact”
uniapp使用vuex
uniapp是默认支持vuex的
cart.js
export default {
namespaced: true,
state: {
list: ['测试数据,后面删除']
},
mutations:{
add(state,payload) {
state.list.push(payload)
}
}
}
全局getters
export default {
carts: (state) => state.cart.list
}
store入口文件index.js
import cart from './modules/cart.js'
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters.js'
Vue.use(Vuex);//vue的插件机制
//Vuex.Store 构造器选项
const store = new Vuex.Store({
getters,
modules:{
cart
}
})
export default store
项目的入口文件main.js
import store from './store/index.js'
const app = new Vue({
store,
...App
})
app.$mount()
测试使用
import { mapGetters } from 'vuex'
computed: {
...mapGetters(['carts'])
},
methods: {
add() {
this.$store.commit('cart/add', '新数据')
}
}
vuex持久化
直接使用第三方包
安装包:npm i vuex-persistedstate
注意:安装之前先npm init --yes
import cart from './modules/cart.js'
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters.js'
Vue.use(Vuex);//vue的插件机制
++ import createPersistedState from 'vuex-persistedstate'
//Vuex.Store 构造器选项
const store = new Vuex.Store({
++ plugins: [
++ // 可以有多个持久化实例
++ createPersistedState({
++ key: 'app_config_data', // 状态保存到本地的 key
++ paths: ['cart'], // 要持久化的状态,在state里面取,如果有嵌套,可以 a.b.c
++ storage: { // 存储方式定义
++ getItem: (key) => uni.getStorageSync(key), // 获取
++ setItem: (key, value) => uni.setStorageSync(key, value), // 存储
++ removeItem: (key) => uni.removeStorageSync(key) // 删除
++ }
++ })
++ ] ,
getters,
modules:{
cart
}
})
console.log(store)
export default store
购物车-添加
实现步骤
- 定义购物车列表变量
- 绑定点击事件,获取当前商品数据:goods_id, goods_name, goods_price, goods_small_logo, goods_count(数量),goods_checked(是否被选中)
- 存入数组(判断是否加过)
- 加入小红点,提示商品数量信息
代码
补充添加到购物车的逻辑
/packone/goods/index.vue
<!-- 添加数量显示-绝对定位 -->
+ <text class="cart-count" v-if="carts.length">{{carts.length}}</text>
<!-- 进入购物车 -->
<text class="cart icon-cart" @click="goCart">购物车</text>
<!-- 添加商品 -->
+ <text @click="addCart" class="add">加入购物车</text>
补充购物车数据
computed:{
...mapGetters(['cart'])
}
补充添加购物车的回调函数
add(state,payload) {
let good = state.list.find(item => item.goods_id === payload.goods_id)
// 没有=》新增
if (!good) {
state.list.push(payload)
} else {
// 有, 直接 数量加一
good.goods_count++
}
}
补充goCart方法
goCart() {
// 调用vuex的muations添加到购物车
const goods = {
goods_id: this.goodInfo.goods_id,
goods_name: this.goodInfo.goods_name,
goods_price:this.goodInfo.goods_price,
goods_small_logo:this.goodInfo.goods_small_logo,
goods_count:1, // 一次加1件商品
goods_checked: true
}
this.$store.commit('cart/add', goods)
}
购物车-列表渲染
代码
定义计算属性,得到购物车数据
/pages/cart/index.vue
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters(['carts'])
}
}
页面渲染
/pages/cart/cart.vue
<view class="shopname">优购生活馆</view>
<view class="goods" :key="item.goods_id" v-for="(item,index) in carts">
<!-- 商品图片 -->
<image
class="pic"
:src="item.goods_small_logo"
/>
<!-- 商品信息 -->
<view class="meta">
<view class="name">{{item.goods_name}}</view>
<view class="price">
<text>¥</text>{{item.goods_price}}
<text>.00</text>
</view>
购物车-修改数量
实现步骤
- 绑定事件,传入changeCount(索引值,-1)代表加减和索引值
- 更新本地存储数据
- 根据库存处理边界
绑定事件
/pages/cart/cart.vue
<!-- 加减 -->
<view class="amount">
<!-- 绑定事件 -->
<text class="reduce" @click="changeCount(index, -1)">-</text>
<input
type="number"
disabled
v-model="item.goods_count"
class="number"
/>
<text class="plus" @click="changeCount(index, 1)">+</text>
</view>
回调函数
/pages/cart/cart.vue
changeCount(idx, step) {
this.$store.commit('cart/changeCount', {idx, step})
},
补充mutations
export default {
namespaced: true,
state: {
list: []
},
mutations:{
changeCount(state, {idx, step}){
let count = state.list[idx].goods_count;
// 1. 最大3
if (step === 1 && count >= 3) {
return;
} else if (step === -1 && count === 1) {
// 2. 最小1
return;
}
// 执行加减操作
state.list[idx].goods_count += step
},
}
}
购物车-选中状态
实现步骤
- 绑定事件,处理商品选中状态(单选和全选)=》做取反
- 过滤购物车数据,获取当前选中商品数量
处理单选
goods_checked
每件商品都有一个goods_checked属性,用来记录它当前是否被选中。
取反。选中还是没有选中就是颜色的区别
<!-- 选框 -->
<view class="checkbox">
<icon
+ @click="switchSingle(index, !item.goods_checked)"
type="success"
size="20"
+ :color="item.goods_checked?'#ea4451':'#ccc'"
></icon>
</view>
switchSingle(state, {idx, val}){
state.list[idx].goods_checked = val
},
组件中
switchSingle(idx, val) {
this.$store.commit('cart/switchSingle', {idx, val})
},
处理全选
根据当前选中的商品===购物车中商品总数量 =〉取反
<!-- 其它 -->
<view class="extra">
<label
class="checkall"
+ @click="isSelAll=!isSelAll"
>
<icon
type="success"
+ :color="isSelAll?'#ea4451':'#ccc'"
size="20"></icon>全选
</label>
在getters中补充计算属性isSelAll
getters.js
export default {
carts: (state) => state.cart.list,
goodsCount: (state) => state.cart.list.reduce((acc,cur)=>acc+cur.goods_count, 0),
isSelAll(state){
return {
get() {
return state.cart.list.every(item => item.goods_checked)
},
set(val) {
state.cart.list.forEach(item => item.goods_checked = val)
}
}
}
}
购物车-计算总金额(计算属性)
补充使用计算属性
totalMoney
getters.js
selectedCarts: (state) => {
return state.cart.list.filter(item => item.goods_checked)
},
totalMoney(state, getters) {
return getters.selectedCarts.reduce((acc, cur) => acc+cur.goods_price*cur.goods_count, 0)
},
模板
<view class="total">
合计:
<text>¥</text>
<label>{{totalMoney}}</label>
<text>.00</text>
</view>
购物车-设置收货地址
获取当前微信账号的收货地址并渲染
说明:选择和编辑收货地址的页面,由微信客户端提供
微信客户端提供收货地址,有对应的API: uni.chooseAddress
实现步骤
- 定义地址变量
- 使用uni.chooseAddress()获取地址数据
data () {
return {
- // 收货地址
+ address: null
}
},
computed: {
addr() {
return (
this.address &&
this.address.provinceName +
this.address.cityName +
this.address.countyName +
this.address.detailInfo
)
}
},
// method
getAddress() {
uni.chooseAddress({
success: res => {
// console.log(res);
this.address = res;
}
});
}
视图
<!-- 收货信息 -->
<view class="shipment">
<block v-if="address">
<view class="dt">收货人:</view>
<view class="dd meta">
<text class="name">{{address.userName}}</text>
<text class="phone">{{address.telNumber}}</text>
</view>
<view class="dt">收货地址:</view>
<view class="dd">{{addr}}</view>
</block>
<!-- 获取用户地址 -->
<button v-else @click="getAddress">获取收获地址</button>
</view>
微信小程序配置一下
manifest.json中
"mp-weixin" : {
"appid" : "wx9a269d229bbe7bb2",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true,
"permission" : {},
++ "requiredPrivateInfos":[
++ "chooseAddress"
]
},
创建订单-准备
/pages/cart/cart.vue
<view
+ @click="createOrder"
class="pay">
结算({{checkedPrd.length}})
</view>
// 创建订单
createOrder() {
// 有收货地址和选中至少一件商品
if (!this.address || !this.checkedPrd.length) {
return uni.showToast({
title: "请填写收货地址和添加商品!",
icon: "none"
});
}
// 是否登录
if (!uni.getStorageSync("token")) {
// 跳转登录页
return uni.navigateTo({
url: "/packageone/auth/auth"
});
}
// 调用接口:创建订单
// ...
}
我的-个人中心
一个全新的页面
模板
/pages/profile/profile.vue
<template>
<view class="wrapper">
<!-- 个人资料 -->
<view class="profile">
<view class="meta">
<image
class="avatar"
:src="avatarUrl"
/>
<text class="nickname">{{nickName}}</text>
</view>
</view>
<!-- 统计 -->
<view class="count">
<view class="cell">
8
<text>收藏的店铺</text>
</view>
<view class="cell">
14
<text>收藏的商品</text>
</view>
<view class="cell">
18
<text>关注的商品</text>
</view>
<view class="cell">
84
<text>我的足迹</text>
</view>
</view>
<!-- 我的订单 -->
<view class="orders">
<view class="title">我的订单</view>
<view class="sorts">
<text class="icon-bill">待付款</text>
<text class="icon-car">待收货</text>
<text class="icon-money">退款/退货</text>
<text class="icon-list">全部订单</text>
</view>
</view>
<!-- 地址管理 -->
<view class="address icon-arrow">收货地址</view>
<!-- 其它 -->
<view class="extra">
<view class="item icon-arrow">联系客服</view>
<button class="item icon-arrow">分享优购</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
avatarUrl: 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0',
nickName: '点击登录'
}
},
methods: {
hLogin() {
uni.navigateTo({
url:'/packageone/auth/auth'
})
}
}
};
</script>
<style scoped lang="scss">
.wrapper {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
background-color: #f4f4f4;
}
.profile {
height: 375rpx;
background-color: #ea4451;
display: flex;
justify-content: center;
align-items: center;
.meta {
.avatar {
width: 140rpx;
height: 140rpx;
border-radius: 50%;
border: 2rpx solid #fff;
}
.nickname {
display: block;
text-align: center;
margin-top: 20rpx;
font-size: 30rpx;
color: #fff;
}
}
}
.count {
display: flex;
margin: 0 20rpx;
height: 100rpx;
text-align: center;
border-radius: 4rpx;
background-color: #fff;
position: relative;
top: -27rpx;
.cell {
flex: 1;
padding-top: 16rpx;
font-size: 27rpx;
color: #333;
}
text {
display: block;
font-size: 24rpx;
}
}
.orders {
margin: -17rpx 20rpx 0 20rpx;
padding: 20rpx 0;
background-color: #fff;
border-radius: 4rpx;
.title {
padding-left: 20rpx;
font-size: 30rpx;
color: #333;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #eee;
}
.sorts {
padding-top: 30rpx;
text-align: center;
display: flex;
}
[class*="icon-"] {
flex: 1;
font-size: 24rpx;
&::before {
display: block;
font-size: 48rpx;
margin-bottom: 8rpx;
color: #ea4451;
}
}
}
.address {
line-height: 1;
background-color: #fff;
font-size: 30rpx;
padding: 25rpx 0 25rpx 20rpx;
margin: 10rpx 20rpx;
color: #333;
border-radius: 4rpx;
}
.extra {
margin: 0 20rpx;
background-color: #fff;
border-radius: 4rpx;
.item {
line-height: 1;
padding: 25rpx 0 25rpx 20rpx;
border-bottom: 1rpx solid #eee;
font-size: 30rpx;
color: #333;
}
button {
text-align: left;
background-color: #fff;
&::after {
border: none;
border-radius: 0;
}
}
}
.icon-arrow {
position: relative;
&::before {
position: absolute;
top: 50%;
right: 20rpx;
transform: translateY(-50%);
}
}
</style>
使用API能力拨打电话
<view @click="callSer" class="item icon-arrow">联系客服</view>
callSer() {
uni.makePhoneCall({
phoneNumber: "10086"
});
}
分享小程序
<button class="item icon-arrow" open-type="share">分享优购</button>
登录支付
auth
初始模板
<template>
<view class="auth">
<view class="title">ugo-登录</view>
<button class="loginBtnAvatar" open-type="chooseAvatar">
<image class="avatar" :src="avatarUrl"></image>
</button>
<view>
<input type="nickname" placeholder="点击修改昵称" v-model="nickName"/>
</view>
<button class="loginBtn" size="mini" @click="hLogin" type="primary">
登录
</button>
</view>
</template>
<script>
export default {
data() {
return {
avatarUrl: 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0',
nickName: ''
}
},
methods: {
hLogin(user) {
// 实现登录功能
}
}
}
</script>
<style lang="scss">
.auth {
padding:50rpx;
text-align: center;
.title{
margin: 40rpx auto;
}
.loginBtnAvatar {
width: 200rpx;
height: 200rpx;
border-radius: 50%;
padding:0px;
margin: 40rpx auto;
}
.avatar {
width: 200rpx;
height: 200rpx;
border-radius: 50%;
border: 2rpx solid #fff;
}
.nickname {
display: block;
text-align: center;
margin-top: 20rpx;
font-size: 30rpx;
color: #fff;
}
.loginBtn {
width: 200rpx;
margin: 50rpx auto;
display: block;
}
}
</style>
获取用户头像
模板:添加chooseavatar事件回调
<button class="loginBtnAvatar" open-type="chooseAvatar" @chooseavatar="getAvatar">
代码
补充getAvatar功能
methods: {
getAvatar(e){
console.log(e)
this.avatarUrl = e.detail.avatarUrl
}
}
微信登录-流程
流程图
名词解释:
code
临时登录凭证, 有效期由微信官方决定, 通过wx.login()
获取session_key
会话密钥, 服务端通过 code2Session 获取(后端负责处理)openId
用户在该小程序下的用户唯一标识, 永远不变, 服务端通过 code 获取unionId
用户在同一个微信开放平台帐号(公众号, 小程序, 网站, 移动应用)下的唯一标识, 永远不变appId
小程序唯一标识appSecret
小程序的 app secret, 可以和 code, appId 一起换取 session_key
其他名词
rawData
不包括敏感信息的原始数据字符串,用于计算签名encryptedData
包含敏感信息的用户信息, 是加密的signature
用于校验用户信息是否无篡改iv
加密算法的初始向量
扩展阅读:微信登录
微信登录-实操
实现步骤
- 通过getUserProfile方法,获取微信用户信息
- 使用微信用户信息来调用ugo的接口登录,获取token
- 根据登录接口文档所需参数,调用接口
<script>
export default {
data() {
return {
avatarUrl: 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0',
nickName: ''
}
},
onShow() {
const userInfo = uni.getStorageSync('userInfo') || {}
this.avatarUrl = userInfo.avatarUrl
this.nickName = userInfo.nickName
},
async onLoad() {
const res = await uni.login()
console.log(res)
this.code = res.code
},
methods: {
getAvatar(e){
console.log(e)
this.avatarUrl = e.detail.avatarUrl
},
// 调用接口登录
async hLogin(user) {
// 登录所需参数
const res = await uni.getUserProfile({
desc: '获取微信用户信息'
});
console.log(res)
const {
encryptedData,
iv,
rawData,
signature
} = res
const {data} = await this.request({
url: "/users/wxlogin",
method: "post",
data: {
encryptedData,
iv,
rawData,
signature,
code: this.code
}
});
console.log(data);
// 保存token
uni.setStorageSync("token", data.token)
// 保存用户更新后的头像和昵称 -- 只是纯前端处理,服务器中并没有对应的接口来保存修改结果
uni.setStorageSync("userInfo", {avatarUrl: this.avatarUrl, nickName: this.nickName})
// 后退
uni.navigateBack()
}
}
}
</script>
异常情况说明
登录需要的后端接口appid: wxfb52f2d7b2f6123a(需要该appid拥有者添加开发者权限)调用失败,是由于当前开发者AppID和服务器端使用的不一致造成
实际工作中,开发小程序=》向公司提供自己的微信号,管理员添加开发者权限=》使用公司的appId进行小程序的开发需要使用和后端接口一致的 appid
同学们没法调用登录接口(大家不是本小程序的开发者)获取token,直接使用老师提供的token开发即可
注意⚠️:如果出现Error: Illegal Buffer
错误说明
创建订单-实现
实现步骤
- 处理创建订单接口需要的参数
https://www.showdoc.com.cn/128719739414963/2612148628877795 - 创建订单
- 更新本地购物车
- 成功跳转到订单列表页
代码
/pages/cart/cart.vue
// 创建订单
async createOrder() {
// 有收货地址和选中至少一件商品
if (!this.address || !this.selectedCarts.length) {
return uni.showToast({
title: "请填写收货地址和添加商品!",
icon: "none"
});
}
// 是否登录
if (!uni.getStorageSync("token")) {
// 跳转登录页
return uni.navigateTo({
url: "/packageone/auth/auth"
});
}
// 调用接口:创建订单
await this.request({
url: "/my/orders/create",
method: "post",
data: {
order_price: this.totalMoney,
consignee_addr: this.addr,
goods: this.selectedCarts.map(item => {
item.goods_number = item.goods_count;
return item;
})
}
});
// 订单成功创建:清空购物车中已经被提交的数据
this.$store.commit('cart/removeSelected')
// 2. 跳转到订单页面
uni.navigateTo({
url: '/packageone/order/order'
})
}
添加mutations
在cart.js中补充mutations
store/modules/cart.js
mutations: {
// 省略其他...
removeSelected:(state) => {
state.list = state.list.filter(it => !it.goods_checked)
}
}
订单列表
获取订单列表数据并渲染.它是一个独立的页面,需要在packageone包中配置
<template>
<view class="wrapper">
<!-- 订单状态 -->
<view class="tabs">
<text class="active">全部</text>
<text>待付款</text>
<text>已付款</text>
<text>退款/退货</text>
</view>
<!-- 订单 -->
<scroll-view class="orders" scroll-y>
<view class="item">
<!-- 订单中包含哪些商品-->
<block>
<!-- 商品图片 -->
<image class="pic" src="http://static.botue.com/ugo/uploads/goods_1.jpg" />
<!-- 商品信息 -->
<view class="meta">
<view class="name">【海外购自营】黎珐(ReFa) MTG日本 CARAT铂金微电流瘦脸瘦身提拉紧致V脸美容仪 【保税仓发货】</view>
<view class="price">
<text>¥</text>1399
<text>.00</text>
</view>
<view class="num">x1</view>
</view>
</block>
<!-- 总价 -->
<view class="amount">共1件商品 总计: ¥4099(含运费0.00)</view>
<!-- 其它 -->
<view class="extra">
订单号: GD20180511000000000178
<button size="mini" type="primary">支付</button>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
export default {};
</script>
<style scoped lang="scss">
.tabs {
display: flex;
height: 96rpx;
line-height: 96rpx;
background-color: #fff;
box-shadow: 0 4rpx 10rpx #ccc;
text {
flex: 1;
text-align: center;
font-size: 27rpx;
color: #333;
&.active {
color: #ea4451;
}
}
}
.orders {
width: 100%;
background-color: #f4f4f4;
position: absolute;
top: 97rpx;
bottom: 0;
}
.item {
padding: 30rpx 20rpx 0;
margin-top: 16rpx;
background-color: #fff;
.pic {
width: 200rpx;
height: 200rpx;
float: left;
}
.meta {
height: 200rpx;
margin-left: 230rpx;
font-size: 27rpx;
color: #333;
position: relative;
}
.name {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.price {
position: absolute;
bottom: 0;
color: #ea4451;
font-size: 33rpx;
text {
font-size: 22rpx;
}
}
.num {
position: absolute;
bottom: 0;
right: 20rpx;
color: #333;
}
.amount {
text-align: right;
padding: 20rpx;
font-size: 24rpx;
border-top: 1rpx solid #eee;
border-bottom: 1rpx solid #eee;
margin-top: 20rpx;
color: #999;
}
.extra {
padding: 30rpx;
font-size: 24rpx;
color: #999;
position: relative;
button {
position: absolute;
right: 20rpx;
font-size: 24rpx;
margin-top: -10rpx;
}
}
}
</style>
实现步骤
- 判断是否登录,没有跳转=》登录页面
- 调用接口获取所有订单数据并绑定模版渲染
补充数据项
/packageone/order/order.vue
data() {
return {
// 订单列表数据
orders: []
};
},
获取订单数据
onShow() {
// 判断是否登录
if (!uni.getStorageSync("token")) {
return uni.navigateTo({ url: "/package/auth/auth" });
}
this.getOrders()
},
methods:{
async getOrders() {
// 获取订单列表
let { msg, data } = await this.request({
url: "/my/orders/all",
data: {
type: 1 // type为1表示获取全部的订单
}
});
this.orders = data.orders;
}
}
页面渲染
<!-- 订单 -->
<scroll-view class="orders" scroll-y>
<!-- 订单列表 -->
<view class="item" v-for="order in orders" :key="order.order_number">
<!-- 订单中包含哪些商品 -->
<block v-for="prd in order.goods" :key="prd.goods_id">
<!-- 商品图片 -->
<image class="pic" :src="prd.goods_small_logo" />
<!-- 商品信息 -->
<view class="meta">
<view class="name">{{ prd.goods_name }}</view>
<view class="price">
<text>¥</text>{{ prd.goods_price }}
<text>.00</text>
</view>
<view class="num">x{{ prd.goods_number }}</view>
</view>
</block>
<!-- end -->
<!-- 总价 -->
<view class="amount">
共{{ order.goods.length }}件商品 总计: ¥{{order.total_price}}(含运费0.00)
</view>
<!-- 其它 -->
<view class="extra">
订单号: {{ order.order_number }}
<button size="mini" type="primary">支付</button>
</view>
</view>
</scroll-view>
微信支付-流程分析
了解小程序支付流程。支付业务流程
微信支付,在小程序端要做三件事:
- 使用
**wx.login**
获取临时登录凭证code,发送到后端获取openId=》微信登录 - 将
**openId**
以及相应需要的商品信息发送到后端,换取服务端进行支付的签名等信息=》创建订单 - 接收返回的信息(必须要包含发起微信支付
**wx.requestPayment的参数**
),发起微信支付
前端:主要处理第三步,获取支付信息,调起支付窗口
微信支付-实现
整体步骤
- 创建订单
-
- 请求创建订单的 API 接口:把(订单金额、收货地址、订单中包含的商品信息)发送到服务器
- 服务器响应的结果:订单编号
- 订单预支付
-
- 请求订单预支付的 API 接口:把(订单编号)发送到服务器
- 服务器响应的结果:订单预支付的参数对象,里面包含了订单支付相关的必要参数
- 发起微信支付
-
- 把步骤 2 得到的 “订单预支付对象” 作为参数传递给
uni.requestPayment()
方法实现付款
- 把步骤 2 得到的 “订单预支付对象” 作为参数传递给
核心代码
模板绑定点击事件
/packageone/order/order.vue
<!-- 其它 -->
<view class="extra">
订单号: {{order.order_number}}
<button
+ @click="pay(order.order_number)"
size="mini"
type="primary"
>支付</button>
</view>
代码中的处理逻辑
- 调用后台支付接口,传入订单号
- 成功后,调起微信支付窗口=>uni.requestPayment
/packageone/order/order.vue
// 支付
async pay(order_number) {
// 1. 发送支付请求
let { data } = await this.request({
url: "/my/orders/req_unifiedorder",
method: "post",
data: {
order_number
}
});
// 2. 调起微信支付窗口,等待用户付钱
await uni.requestPayment(data.pay);
uni.showToast({
title: "微信支付成功!",
duration: 2000
});
await this.request({
url: "/my/orders/chkOrder",
method: "post",
data: {
order_number: 'HMDD20230624000000050366'
}
});
//检测到订单支付完成
uni.showToast({
title: '订单完成!',
icon: 'success'
})
},
扩展阅读:https://uniapp.dcloud.io/api/plugins/payment?id=requestpayment
项目打包-上线
实现步骤
- 打包
- hbuildX:发布
- vue-cli: 执行上线打包:
npm run build:mp-weixin
-
到微信开发者工具,导入打包生成的build/mp-weixin生产代码
-
导入成功之后点击=》上传=》发布体验版本 =》 通过小程序的后台查看管理版本
-
体验版本=》经过测试=》测试通过=》提交审核=》经过TX审核通过才能发布线上版本
小程序怎么做支付(例如在商场小程序A买东西)
-
登录 小程序A,拿到token
-
- uni.login --------> code
- uni.getUserInfo ------> xx,xxx,xx,xxx
- 调用后端给定接口,把上面两步的参数传入,就可以拿到token
2.创建订单
-
- 调用后端给定接口(带token)
-
支付订单(根据订单号)
-
- 调用后端给定接口(带token,带订单号 ) -------> 支付信息 pay
- 调用uni.requestpayment(pay信息) ------> 弹框(模拟器:二维码,真机:支付界面)----> 用户确认付钱!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bcikGVdH-1688129651236)(C:\Users\ZhengKaiYa\Desktop\uniapp1\26.png)]
搜索框防抖
<input @input="hRearch">
hRearch(){}{
如果搜索框有内容,就发请求()
}
// 防抖
hRearch(){}{
clearTimeout(this.定时器ID)
this.定时器ID = setTimeout(() => {
如果搜索框有内容,就发请求()
}, 500)
}
// 节流
hRearch(){}{
if(Date.now() - this.上次执行时间 > 500) {
如果搜索框有内容,就发请求()
this.上次执行时间 = Date.now()
}
}
ds_name }}
x{{ prd.goods_number }}
共{{ order.goods.length }}件商品 总计: ¥{{order.total_price}}(含运费0.00)
订单号: {{ order.order_number }}
支付
## 微信支付-流程分析
了解小程序支付流程。[支付业务流程](https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_4&index=3)
[外链图片转存中...(img-JItQYFWW-1688129651235)]
微信支付,在小程序端要做三件事:
1. 使用`**wx.login**`获取临时登录凭证code,发送到后端获取openId=》微信登录
2. 将`**openId**`以及相应需要的商品信息发送到后端,换取服务端进行支付的签名等信息=》创建订单
3. 接收返回的信息(必须要包含发起微信支付`**wx.requestPayment的参数**`),发起微信支付
前端:主要处理第三步,获取支付信息,调起支付窗口
## 微信支付-实现
**整体步骤**
1. **创建订单**
- - 请求创建订单的 API 接口:把(订单金额、收货地址、订单中包含的商品信息)发送到服务器
- 服务器响应的结果:*订单编号*
1. **订单预支付**
- - 请求订单预支付的 API 接口:把(订单编号)发送到服务器
- 服务器响应的结果:*订单预支付的参数对象*,里面包含了订单支付相关的必要参数
1. **发起微信支付**
- - 把步骤 2 得到的 “订单预支付对象” 作为参数传递给 `uni.requestPayment()` 方法实现付款
**核心代码**
模板绑定点击事件
/packageone/order/order.vue
```js
<!-- 其它 -->
<view class="extra">
订单号: {{order.order_number}}
<button
+ @click="pay(order.order_number)"
size="mini"
type="primary"
>支付</button>
</view>
代码中的处理逻辑
- 调用后台支付接口,传入订单号
- 成功后,调起微信支付窗口=>uni.requestPayment
/packageone/order/order.vue
// 支付
async pay(order_number) {
// 1. 发送支付请求
let { data } = await this.request({
url: "/my/orders/req_unifiedorder",
method: "post",
data: {
order_number
}
});
// 2. 调起微信支付窗口,等待用户付钱
await uni.requestPayment(data.pay);
uni.showToast({
title: "微信支付成功!",
duration: 2000
});
await this.request({
url: "/my/orders/chkOrder",
method: "post",
data: {
order_number: 'HMDD20230624000000050366'
}
});
//检测到订单支付完成
uni.showToast({
title: '订单完成!',
icon: 'success'
})
},
扩展阅读:https://uniapp.dcloud.io/api/plugins/payment?id=requestpayment
项目打包-上线
实现步骤
- 打包
- hbuildX:发布
- vue-cli: 执行上线打包:
npm run build:mp-weixin
-
到微信开发者工具,导入打包生成的build/mp-weixin生产代码
-
导入成功之后点击=》上传=》发布体验版本 =》 通过小程序的后台查看管理版本
-
体验版本=》经过测试=》测试通过=》提交审核=》经过TX审核通过才能发布线上版本
小程序怎么做支付(例如在商场小程序A买东西)
-
登录 小程序A,拿到token
-
- uni.login --------> code
- uni.getUserInfo ------> xx,xxx,xx,xxx
- 调用后端给定接口,把上面两步的参数传入,就可以拿到token
[外链图片转存中…(img-SfGZ2WYw-1688129651236)]
2.创建订单
-
- 调用后端给定接口(带token)
-
支付订单(根据订单号)
-
- 调用后端给定接口(带token,带订单号 ) -------> 支付信息 pay
- 调用uni.requestpayment(pay信息) ------> 弹框(模拟器:二维码,真机:支付界面)----> 用户确认付钱!
[外链图片转存中…(img-bcikGVdH-1688129651236)]
搜索框防抖
<input @input="hRearch">
hRearch(){}{
如果搜索框有内容,就发请求()
}
// 防抖
hRearch(){}{
clearTimeout(this.定时器ID)
this.定时器ID = setTimeout(() => {
如果搜索框有内容,就发请求()
}, 500)
}
// 节流
hRearch(){}{
if(Date.now() - this.上次执行时间 > 500) {
如果搜索框有内容,就发请求()
this.上次执行时间 = Date.now()
}
}