title: uni-app(优医咨询)项目实战 - 第3天
series: uni-app(优医咨询)项目实战
abbrlink: ‘46e8593’
date: 2024-04-17 10:58:42
uni-app(优医咨询)项目实战 - 第3天
学习目标:
- 掌握 luch-request 网络请求的用法
- 能够对 Pinia 进行初始化操作
- 掌握创建 Store 及数据操作的步骤
- 能够对 Pinia 数据进行持久化的处理
- 掌握用户登录的实现方法
一、项目启动
从零起步创建项目,完整的静态页面可以从 gitee 仓库获取。
1.1 创建项目
以 HBuilder X 的方式创建项目:
- 项目名称:优医咨询
- Vue 版本:Vue3
- 模板:默认模板
1.1.1 .prettierrc
在项目根目录下创建 .prettierrc
文件,然后添加下述配置选项:
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"vueIndentScriptAndStyle": true,
}
上述配置内容是关于 Prettier 的常用的配置项,以后实际开发过程中可以根据需要逐步完善。
1.1.2 配置 tabBar
根据设计稿的要求配置 tabBar,首先通过 HBuilder X 新建 3 个页面,然后再配置 pages.json
文件。
共有4个页面,分别为:首页、健康百科、消息通知、我的,在课堂上统一约束目录的名称:首页对应 index、健康百科对应 wiki、消息通知对应 notify、我的对应 my 。
tabBar 用的图片在课程资料中可以找到,将其拷贝到项目的根目录下,然后在 pages.json
中进行配置:
{
"pages": [{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "优医咨询"
}
}, {
"path": "pages/my/index",
"style": {
"navigationBarTitleText": "我的",
"enablePullDownRefresh": false
}
}, {
"path": "pages/notify/index",
"style": {
"navigationBarTitleText": "消息通知",
"enablePullDownRefresh": false
}
}, {
"path": "pages/wiki/index",
"style": {
"navigationBarTitleText": "健康百科",
"enablePullDownRefresh": false
}
}],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "优医咨询",
"navigationBarBackgroundColor": "#fff",
"backgroundColor": "#F8F8F8"
},
"tabBar": {
"color": "#6F6F6F",
"selectedColor": "#6F6F6F",
"borderStyle": "white",
"list": [{
"text": "首页",
"pagePath": "pages/index/index",
"iconPath": "static/tabbar/home-default.png",
"selectedIconPath": "static/tabbar/home-active.png"
},
{
"text": "健康百科",
"pagePath": "pages/wiki/index",
"iconPath": "static/tabbar/wiki-default.png",
"selectedIconPath": "static/tabbar/wiki-active.png"
},
{
"text": "消息通知",
"pagePath": "pages/notify/index",
"iconPath": "static/tabbar/notify-default.png",
"selectedIconPath": "static/tabbar/notify-active.png"
},
{
"text": "我的",
"pagePath": "pages/my/index",
"iconPath": "static/tabbar/my-default.png",
"selectedIconPath": "static/tabbar/my-active.png"
}
]
},
"uniIdRouter": {}
}
除了配置 tabBar
外,还要配置每个页面的导航栏的标题 navigationBarTitleText
及全局导航栏背景颜色 navigationBarBackgroundColor
为白色。
1.1.3 公共样式
在 App.vue 中配置公共 css 代码,不仅能精简代码,将来样式的维护也会更方便,这些公共样式是由开发者根据不同的项目需要自定义的,因此不同的项目或者不同开发者定义的公共样式是不一致的,本项目中我定义了以下部分的公共样式:
<!-- App.vue -->
<script>
// 省略这里的代码...
</script>
<style lang="scss">
image {
vertical-align: middle;
}
button:after {
display: none;
}
.uni-button {
height: 88rpx;
text-align: center;
line-height: 88rpx;
border-radius: 88rpx;
color: #fff;
font-size: 32rpx;
background-color: #20c6b2;
&[disabled],
&.disabled {
color: #fff !important;
background-color: #ace8e0 !important;
}
}
</style>
关于 scss 本项目定义了一个变量和一个混入,这个混入是用来处理文字溢出的,溢出的部分会显示 ...
来代替。
// uni.scss
// 省略了默认生成的 scss 代码...
$line: 2;
@mixin text-overflow($line) {
display: -webkit-box;
-webkit-line-clamp: $line;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
overflow: hidden;
}
1.1.4 引入字体图标
项目中即用到了单色图标,也用到了多色图标:
- 单色图标,将字体图标文件解压缩到 static/fonts 目录中,将 iconfont.css 重命名为 iconfont.scss
@font-face {
font-family: 'iconfont';
src: url('/static/fonts/iconfont.ttf') format('truetype');
}
.iconfont {
font-family: 'iconfont' !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-done:before {
content: '\ea54';
}
.icon-location:before {
content: '\e6ea';
}
.icon-edit:before {
content: '\e6e9';
}
.icon-shield:before {
content: '\e6e8';
}
.icon-checked:before {
content: '\e6e5';
}
.icon-box:before {
content: '\e6e6';
}
.icon-truck:before {
content: '\e6e7';
}
图标成功导入项目后,在 App.vue 中导入自定义图标的样式文件
<!-- App.vue -->
<script>
// 省略这里的代码...
</script>
<style lang="scss">
// 单色图标
@import '@/static/fonts/iconfont.scss'
// 以下部分代码省略...
</style>
字体图标导入成功后要到页面测试一下图标是否能正常显示。
- 关于多色图标的使用在前面课程中已经介绍过了,关于图标的转换部分就不再演示了,我们直接将转换后代码引入项目中
先将生成的多色图标文件 color-fonts.scss
放到项目的根目录中,然后在 App.vue 中导入该文件
<!-- App.vue -->
<script>
// 省略这里的代码...
</script>
<style lang="scss">
// 单色图标
@import '@/static/fonts/iconfont.scss';
// 多色图标
@import './color-fonts.scss';
// 以下部分代码省略...
</style>
字体图标导入成功后要到页面测试一下图标是否能正常显示。
1.1.5 网站图标
浏览器在加载网页时会在标签页位置展示一个小图标,我们来指定一下这个图标:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- 这里省略了部分代码... -->
<!-- 这行代码用来指定网站图标 -->
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>
1.2 公共封装
封装一系列的公共的方法,如网络请求、轻提示、日期时间处理等。
1.2.1 网络请求
小程序或 uni-app 提供了专门用于网络请求的 API ,但结合实际开发还需要扩展一些与业务相关的逻辑,如基地址、拦截器等功能,通常会对 uni.request
进行封装,luch-request
就是这样一个工具模块,它仿照 axios 的用法对 uni.request
进行二次封装,扩展了基地址、拦截器等业务相关的功能。
- 安装
luch-request
npm install luch-request
- 实例化并配置基地址,项目根目录新建 utils/http.js
// utils/http.js
// 导入模块
import Request from 'luch-request'
// 实例化网络请求
const http = new Request({
// 接口基地址
baseURL: 'https://t1ps66c7na.hk.aircode.run',
})
// 导出配置好的模网络模块
export { http }
<!-- pages/test/index.vue -->
<script setup>
import { http } from '@/utils/http.js'
function onButtonClick() {
// 1. 普通用法
http.request({
url: '/echo',
method: 'GET',
header: {
customHeader: '22222222'
}
})
}
</script>
<template>
<view class="content">
<button @click="onButtonClick" type="primary">luch-request 测试</button>
</view>
</template>
- 配置请求拦截器
在请求之前执行一些逻辑,例如检测登录状态,添加自定义头信息等。
// utils/http.js
// 导入模块
import Request from 'luch-request'
// 实例化网络请求
const http = new Request({
// 接口基地址
baseURL: 'https://t1ps66c7na.hk.aircode.run',
})
// 请求拦截器
http.interceptors.request.use(
function (config) {
// 定义头信息,并保证接口调用传递的头信息
// 能够覆盖在拦截器定义的头信息
config.header = {
Authorization: '11111111',
...config.header,
}
return config
},
function (error) {
return Promise.reject(error)
}
)
// 导出配置好的模网络模块
export { http }
以上代码中要注意拦截器中配置的头信息不要将原有的头信息覆盖。
- 配置响应拦截器
// utils/http.js
// 导入模块
import Request from 'luch-request'
// 实例化网络请求
const http = new Request({
// 接口基地址
baseURL: 'https://t1ps66c7na.hk.aircode.run',
})
// 请求拦截器
http.interceptors.request.use(
function (config) {
// 定义头信息,并保证接口调用传递的头信息
// 能够覆盖在拦截器定义的头信息
config.header = {
Authorization: '11111111',
...config.header,
}
return config
},
function (error) {
return Promise.reject(error)
}
)
// 响应拦截器
http.interceptors.response.use(
function ({ statusCode, data, config }) {
// 解构出响应主体
return data
},
function (error) {
return Promise.reject(error)
}
)
// 导出配置好的模网络模块
export { http }
<!-- pages/test/index.vue -->
<script setup>
import { http } from '@/utils/http.js'
async function onButtonClick() {
// 1. 普通用法
const result = await http.request({
url: '/echo',
method: 'GET',
header: {
customHeader: '22222222'
}
})
console.log(result)
}
</script>
<template>
<view class="content">
<button @click="onButtonClick" type="primary">luch-request 测试</button>
</view>
</template>
- 请求加载状态
在发请求之前展示一个加载提示框,请求结束后隐藏这个提示框,该部分的逻辑分别对应请求拦截器和响应拦截器,在请求拦截器中调用 uni.showLoading
在响应拦截器中调用 uni.hideLoading
。
在设置加载提示框之前先来了解一下 luch-request
提供的自定义配置参数的功能,即 custom
属性,该属性的用法如下:
// utils/http.js
// 导入模块
import Request from 'luch-request'
// 实例化网络请求
const http = new Request({
// 接口基地址
baseURL: 'https://t1ps66c7na.hk.aircode.run',
custom: {
abc: 123,
loading: true
}
})
// 省略以下部分代码...
局部配置了相同的自定义参数时会覆盖全局配置的自定义参数
<!-- pages/test/index -->
<script setup>
import { http } from '@/utils/http.js'
async function onButtonClick() {
// 1. 普通用法
const result = await http.request({
// 省略部分代码...
// 局部配置自定义参数
custom: {
abc: 123,
},
// 省略部分代码...
})
console.log(result)
}
</script>
在了解自定义参数的使用后,我们来自定义一个能控制是否需要 loading 提示框的属性,全局默认为 true
。
// utils/http.js
// 导入模块
import Request from 'luch-request'
// 实例化网络请求
const http = new Request({
// 接口基地址
baseURL: 'https://t1ps66c7na.hk.aircode.run',
custom: {
loading: true
}
})
// 请求拦截器
http.interceptors.request.use(
function (config) {
// 显示加载状态提示
if (config.custom.loading) {
uni.showLoading({ title: '正在加载...', mask: true })
}
// 定义头信息,并保证接口调用传递的头信息
// 能够覆盖在拦截器定义的头信息
config.header = {
Authorization: '11111111',
...config.header,
}
return config
},
function (error) {
return Promise.reject(error)
}
)
// 响应拦截器
http.interceptors.response.use(
function ({ statusCode, data, config }) {
// 隐藏加载状态提示
uni.hideLoading()
// 解构出响应主体
return data
},
function (error) {
return Promise.reject(error)
}
)
// 导出配置好的模网络模块
export { http }
到此关于网络请求的基本用法就封装完毕了,后续会补充登录权限检测的业务逻辑。
1.2.2 轻提示
uni-app 提供了 uni.showToast
API 用于轻提示,但其传的参数比较复杂,通过封装来简化参数的传递。
新建 utils/utils.js
/**
* 项目中会用的一系列的工具方法
*/
uni.utils = {
/**
* 用户反馈(轻提示)
* @param {string} title 提示文字内容
* @param {string} icon 提示图标类型
*/
toast(title = '数据加载失败!', icon = 'none') {
uni.showToast({
title,
icon,
mask: true,
})
},
}
这里的方法将来是会被全局引用的,因此在入口 main.js
中导入 utils/utils.js
import { createSSRApp } from 'vue'
import App from './App'
import '@/utils/utils'
export function createApp() {
const app = createSSRApp(App)
return {
app,
}
}
在入口文件 main.js 中使用条件编译兼容了 Vue2 和 Vue3,由于本项目确定了要使用 Vue3 且会用到组合式 API,因此可以将 Vu2 部分的代码删除掉。
将来就可以任意位置来使用 utils 的封装了,用法如下所示:
<!-- pages/test/index.vue -->
<script setup>
import { http } from '@/utils/http.js'
async function onButtonClick() {
// 1. 普通用法
const result = await http.request({
url: '/echo',
// 省略这里的代码...
})
// 这是工具方法的用法
uni.utils.toast('测试轻提示')
}
</script>
在以后的开发中还会根据需要扩充更多的方法。
二、Pinia 状态管理
Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。
Pinia 起源于一次探索 Vuex 下一个迭代的实验,因此结合了 Vuex 5 核心团队讨论中的许多想法。最后,我们意识到 Pinia 已经实现了我们在 Vuex 5 中想要的大部分功能,所以决定将其作为新的推荐方案来代替 Vuex。
Vuex 3.x 只适配 Vue 2,而 Vuex 4.x 是适配 Vue 3 的,Pinia 可以同时支持 Vue2 和 Vue3。
2.1 安装
# 或者使用其它包管理工具,如 yarn pnpm
npm install pinia
创建一个 pinia 实例 (根 store) 并将其传递给应用:
// main.js
import { createSSRApp } from 'vue'
// 导入 Pinia
import { createPinia } from 'pinia'
import App from './App'
import '@/utils/utils'
export function createApp() {
const app = createSSRApp(App)
// 创建 Pinia 实例
const pinia = createPinia()
app.use(pinia)
return {
app,
}
}
注意事项:
createSSRApp
是配合 SSR 来使用的,其用法与createApp
相同,在这里 uni-app 为了做跨平台开发所采取的方式,其作用我们就按createApp
来理解即可。
2.2 Store
在深入研究核心概念之前,我们得知道 Store 是用 defineStore()
定义的,它支持两种语法法格式,分别是选项式 Store 和 组件式 Store,创建一个文件来演示它的使用:
- 选项式(Options) Store
// stores/counter.js
import { defineStore } from 'pinia'
// 选项式 Store
export const useCounterStore = defineStore('counter', {
state: () => {
return {
count: 0,
}
},
getters: {
double: (state) => {
return state.count * 2
},
},
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
},
})
在 pages/test/index
页面中通一个计数器来测试它的用法,下面新增的布局相关代码
<!-- pages/test/index.vue -->
<script setup>
import { http } from '@/utils/http.js'
// 测试网络请求
async function onButtonClick() {
// 省略前面小节代码
}
</script>
<template>
<view class="content">
<!-- 前面小节代码省略了 -->
<view class="counter">
<button class="button" type="primary">-</button>
<input class="input" type="text" />
<button class="button" type="primary">+</button>
</view>
</view>
</template>
<style lang="scss">
// 前面小节代码省略了
.counter {
display: flex;
margin-top: 30rpx;
}
.input {
flex: 1;
height: 96rpx;
text-align: center;
border: 2rpx solid #eee;
box-sizing: border-box;
}
.button {
width: 100rpx;
margin: 0;
&:first-child {
border-start-end-radius: 0;
border-end-end-radius: 0;
}
&:last-child {
border-start-start-radius: 0;
border-end-start-radius: 0;
}
}
</style>
接下来看如何使用 Pinia,在使用时要注意必须要调用定义好的 Store 才会真正创建 Store 实例,对应到下面的代码是必须要调用 useCounterStore
后才会创建 Store 实例,即 counterStore
。
<!-- pages/test/index.vue -->
<script setup>
import { http } from '@/utils/http.js'
// 导入定义好的 Store
import { useCounterStore } from '@/stores/counter.js'
// 创建 Store 实例
const counterStore = useCounterStore()
// 测试网络请求
async function onButtonClick() {
// 省略前面小节代码
}
</script>
<template>
<view class="content">
<!-- 前面小节代码省略了 -->
<view class="counter">
<button class="button" type="primary">-</button>
<input class="input" type="text" />
<button class="button" type="primary">+</button>
</view>
</view>
</template>
Store 实例中定义的 state
、getters
、actions
可以直接应用到组件模板当中。
注意事项:
- 定义 Store 时建议(非必须)使用 use + 名称 + Store 格式命名,其中名称也会被当做 ID 出现在调试工具中
- 创建 Store 实例时,实例的名称建议用 名称 + Store 格式命名,避免引入多个 Store 时名称重复的问题
- 组件式(Setup)Store
组合式 Store 用法与选项式 Store 用法最直接的区别就是 defineStore
的第 2 个参数传入是一个函数,而选项式 Store 传入的是一个对象。
另一个区别是在组合式 API 中允许使用 Vue 的组合式函数,如 ref
、computed
、watch
等。
在组件式 Store 中:
ref()
就是state
属性computed
就是getters
function()
就是actions
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// 1. 选项式 Store
// 这里省略上一小节代码
// 2. 组合式 Store
export const useCounterStore = defineStore('counter', () => {
// 定义 state
const count = ref(0)
// 定义 getters
const double = computed(() => count.value * 2)
// 定义 actions
function increment() {
count.value++
}
function decrement() {
count.value--
}
// 千万不要忘记这里要 return
return { count, double, increment, decrement }
})
对比发现组合式 Store 的用法与 Vue 组件的 setup 用法是一致的,咱们项目中会采用这种用法来开发。
2.3 State
State 实际上就是用来共享访问的数据,这些数据会涉及到访问、变更等操作,我们分别来学习 State 的相关操作。
- 访问,State 的数据是使用
reactive
创建的,直接通过 Store 实例属性的方式即可访问,这种访问方式也包括了getters
的访问。
<!-- pages/test/index.vue -->
<script setup>
// 导入定义好的 Store
import { useCounterStore } from '@/stores/counter.js'
// 创建 Store 实例
const counterStore = useCounterStore()
// 像普通过 reactive 包装数据一样来访问
console.log(counterStore.count)
// getters 可以可采用相同的方式来访问
console.log(counterStore.double)
</script>
但是这里一定要注意是 reactive
的数据不允许解构,解构后的数据将会失去响应式,为了解决这个问题可以使用 Pinia 提供的工具函数 storeToRefs
<!-- pages/test/index.vue -->
<script setup>
// 导入工具方法解构 State 数据
import { storeToRefs } from 'pinia'
// 导入定义好的 Store
import { useCounterStore } from '@/stores/counter.js'
// 创建 Store 实例
const counterStore = useCounterStore()
// 直接解构是错误的用法
// const { count, double } = counterStore
// 正确的解构方法
const { count, double } = storeToRefs(counterStore)
</script>
<template>
<view class="content">
<!-- 省略前面小节的代码... -->
<view class="counter">
<button @click="counterStore.decrement" class="button" type="primary">
-
</button>
<input class="input" :value="counterStore.count" type="text" />
<button @click="counterStore.increment" class="button" type="primary">
+
</button>
</view>
<!-- 在这里访问解构后的数据 -->
<view class="state">
<text class="text">count: {{ count }}</text>
<text class="text">double: {{ double }}</text>
</view>
</view>
</template>
- 变更
变更 State 的数据有两种方式,一种是直接赋值,另一种是调用 $patch 方法。
<!-- pages/test/index.vue -->
<script setup>
import { storeToRefs } from 'pinia'
// 导入定义好的 Store
import { useCounterStore } from '@/stores/counter.js'
// 创建 Store 实例
const counterStore = useCounterStore()
let _count = 0
// 更新 state
function increment() {
// 直接等号赋值
// counterStore.count++
// 调用 $patch 方法
counterStore.$patch({
count: ++_count,
})
}
// 更新 state
function decrement() {
// 直接等号赋值
// counterStore.count--
// 调用 $patch 方法
counterStore.$patch({
count: --_count,
})
}
</script>
<template>
<view class="content">
<!-- 省略前面小节部分代码... -->
<view class="counter">
<button @click="decrement" class="button" type="primary">-</button>
<input class="input" :value="counterStore.count" type="text" />
<button @click="increment" class="button" type="primary">+</button>
</view>
<view class="state">
<text class="text">count: {{ count }}</text>
<text class="text">double: {{ double }}</text>
</view>
</view>
</template>
这两种方式都可以用来对 State 数据进行修改,在一次性需要更新多个数据时推荐使用 $patch
方法,单个数据更新时使用等号直接赋值。
2.4 持久化
Pinia 的数据是以全局的方式存储在内存中的,这会导致页面被刷新后数据丢失或重置,但实际开发中有的数据需要长时间的存储,即所谓的持久化,通常都是存入本地存储当中来实现的,在 Pinia 中通过插件来扩展持久化的功能。
- 安装
# 也可以使用其它包管理工具,如 yarn pnpm
npm i pinia-plugin-persistedstate
- 将插件添加 Pinia 实例上
import { createSSRApp } from 'vue'
// 导入 Pinia
import { createPinia } from 'pinia'
// Pinia 持久化插件
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App'
import '@/utils/utils'
export function createApp() {
const app = createSSRApp(App)
// 创建 Pinia 实例
const pinia = createPinia()
// 应用 Pinia 插件
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
return {
app,
}
}
- 将数据持久化存储,为
defineStore
传入第3个参数,第3个参数是对象类型
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// 2. 组合式 Store
export const useCounterStore = defineStore(
'counter',
() => {
// 定义 state
const count = ref(0)
// 定义 getters
const double = computed(() => count.value * 2)
// 定义 actions
function increment() {
count.value++
}
function decrement() {
count.value--
}
// 千万不要忘记这里要 return
return { count, double, increment, decrement }
},
{ persist: true }
)
当 count
数据发生改变后就会将数据存入本地存储当中了,但是这种方式有个弊端就是会将所有 State 数据持久化存储,这样会造成不必要的性能损耗,要解决这个问题也非常方便,通过 paths
来指定需要持久化存储的数据:
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// 2. 组合式 Store
export const useCounterStore = defineStore(
'counter',
() => {
// 定义 state
const count = ref(0)
// 定义 getters
const double = computed(() => count.value * 2)
// 定义 actions
function increment() {
count.value++
}
function decrement() {
count.value--
}
// 千万不要忘记这里要 return
return { count, double, increment, decrement }
},
{
persist: {
paths: ['count'],
},
}
)
- 配置
以上的用法在一般的 Vue 项目中可以满足基本的开发需要了,但是在 uni-app 中时却需要做一些额外的配置,原因在于 uni-app 中本地存储使用的是 uni.setStorageSync
而插件中使用的是 localStorage.setItem
,为此需要我们自定义配置本地址存储的方法。
使用 createPersistedState
进行全局性配置
// main.js
import { createSSRApp } from 'vue'
// 导入 Pinia
import { createPinia } from 'pinia'
// Pinia 持久化插件
import { createPersistedState } from 'pinia-plugin-persistedstate'
// import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App'
import '@/utils/utils'
export function createApp() {
const app = createSSRApp(App)
// 创建 Pinia 实例
const pinia = createPinia()
// 应用 Pinia 插件
// pinia.use(piniaPluginPersistedstate)
pinia.use(
// 自定义 Pinia 插件
createPersistedState({
// 自定义本地存储的逻辑
storage: {
setItem(key, value) {
uni.setStorageSync(key, value)
},
getItem(key) {
return uni.getStorageSync(key)
},
},
})
)
app.use(pinia)
return {
app,
}
}
createPersistedState
传的参数中 storage
是用来自定义持久化存储方法的,其中 setItem
和 getItem
是内置固定的名称,在进行本地存储时插件内部会自动调用这两个方法,进而调用 uni.setStorageSync
将数据存入本地。
另外存入本地数据的名称默认为 Store 的名称,这个名称也允许自定义,使用 key
来指定:
// main.js
import { createSSRApp } from 'vue'
// 导入 Pinia
import { createPinia } from 'pinia'
// Pinia 持久化插件
import { createPersistedState } from 'pinia-plugin-persistedstate'
// import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App'
import '@/utils/utils'
export function createApp() {
const app = createSSRApp(App)
// 创建 Pinia 实例
const pinia = createPinia()
// 应用 Pinia 插件
// pinia.use(piniaPluginPersistedstate)
pinia.use(
// 自定义 Pinia 插件
createPersistedState({
// 自定义本地存数据的名称
key: (id) => `__persisted__${id}`,
// 自定义本地存储的逻辑
storage: {
setItem(key, value) {
uni.setStorageSync(key, value)
},
getItem(key) {
return uni.getStorageSync(key)
},
},
})
)
app.use(pinia)
return {
app,
}
}
三、用户登录
优医问诊提供了 3 种登录方式,分别是用户名和密码、短信验证码、社交账号登录(暂只支持QQ登录),先来实现前两种方式的登录,关于第3方登录我们最后再来实现。
3.1 布局及交互
新建 pages/login/index.vue
页面,创建页面时容易出错的地方是新建的页面路径要添加到 pages.json
文件中,同时将页面志航栏标题设置为用户登录。
用户名&密码方式登录和短信验证码方式登录有一个 Tab 切换显示的的交互,我们大致的实现思路如下 :
3.1.1 布局模板
- 定义 Tab 标签页切换的基础结构
<!-- pages/login/index.vue -->
<script setup></script>
<template>
<view class="user-login">
<view class="login-type">
<view class="title">密码登录</view>
<view class="type">
<text>验证码登录</text>
<uni-icons color="#3c3e42" type="forward" />
</view>
</view>
</view>
<!-- 社交账号登录 -->
<view class="social-login">
<view class="legend">
<text class="text">其它方式登录</text>
</view>
<view class="social-account">
<view class="icon">
<uni-icons color="#00b0fb" size="30" type="qq" />
</view>
<view class="icon">
<uni-icons color="#fb6622" size="30" type="weibo" />
</view>
<view class="icon">
<uni-icons color="#07C160" size="30" type="weixin" />
</view>
</view>
</view>
</template>
<style lang="scss">
@import './index.scss'
</style>
// pages/login/index.scss
.user-login {
padding: 60rpx;
}
.login-type {
display: flex;
align-items: flex-end;
justify-content: space-between;
line-height: 1;
margin: 40rpx 0 80rpx;
.title {
font-size: 48rpx;
color: #121826;
}
.type {
color: #3c3e42;
font-size: 30rpx;
display: flex;
align-items: center;
}
}
.social-login {
margin-top: 100rpx;
padding: 0 60rpx;
.legend {
height: 40rpx;
position: relative;
border-top: 1rpx solid #ebebeb;
}
.text {
position: absolute;
top: -50%;
left: 50%;
font-size: 28rpx;
color: #999;
padding: 0 10rpx;
background-color: #fff;
transform: translate(-50%);
}
.social-account {
display: flex;
justify-content: space-evenly;
margin-top: 40rpx;
.icon {
display: flex;
justify-content: center;
align-items: center;
width: 80rpx;
height: 80rpx;
border-radius: 100rpx;
background-color: #f6f6f6;
}
}
}
注意事项:在以上的页面布局模板中用到了扩展组件 uni ui ,需要安装到项目录中,并重新启动项目。
3.1.2 标签切换
经分析后发现,要在页面中展示不同的文字内容,并且在用户点击后进行切换,实现步骤如下:
- 定义一个对象数组,该数组中包含了要展示在页面中的内容
<!-- pages/login/index.vue -->
<script setup>
import { ref, computed } from 'vue'
// 标签页要展示的内容
const tabMetas = [
{ title: '密码登录', subTitle: '验证码登录' },
{ title: '验证码登录', subTitle: '密码登录' },
]
// 标签页的索引值
const tabIndex = ref(0)
// 根据索引值决定当前标签展示的内容
const tabMeta = computed(() => {
return tabMetas[tabIndex.value]
})
</script>
<template>
<view class="user-login">
<view class="login-type">
<view class="title">{{ tabMeta.title }}</view>
<view class="type">
<text>{{ tabMeta.subTitle }}</text>
<uni-icons color="#3c3e42" type="forward" />
</view>
</view>
</view>
<!-- 社交账号登录 -->
<view class="social-login">
...
</view>
</template>
- 监听点击事件,切根据索引值来切换显示不同的内容
<!-- pages/login/index.vue -->
<script setup>
import { ref, computed } from 'vue'
// 标签页要展示的内容
const tabMetas = [
{ title: '密码登录', subTitle: '验证码登录' },
{ title: '验证码登录', subTitle: '密码登录' },
]
// 标签页的索引值
const tabIndex = ref(0)
// 根据索引值决定当前标签展示的内容
const tabMeta = computed(() => {
return tabMetas[tabIndex.value]
})
// 切换标签页的索引值
function onSubTitleClick() {
// 0 和 1 互换的简单算法
tabIndex.value = Math.abs(tabIndex.value - 1)
}
</script>
<template>
...
</template>
- 封装用户名&密码组件和短信验证码组件,在当前目录下创建
components/mobile.vue
和components/password.vue
组件,组件的布局模板为:
password.vue
<!-- pages/login/components/passoword.vue -->
<script setup></script>
<template>
<uni-forms class="login-form" ref="form">
<uni-forms-item name="mobile">
<uni-easyinput
:input-border="false"
:clearable="false"
placeholder="请输入手机号"
placeholder-style="color: #C3C3C5"
/>
</uni-forms-item>
<uni-forms-item name="password">
<uni-easyinput
type="password"
placeholder="请输入密码"
:input-border="false"
placeholder-style="color: #C3C3C5"
/>
</uni-forms-item>
<view class="agreement">
<radio :checked="false" color="#16C2A3" />
我已同意
<text class="link">用户协议</text>
及
<text class="link">隐私协议</text>
</view>
<button class="uni-button">登 录</button>
<navigator hover-class="none" class="uni-navigator" url=" ">
忘记密码?
</navigator>
</uni-forms>
</template>
<script>
export default {
options: {
styleIsolation: 'shared',
},
}
</script>
<style lang="scss">
@import './styles.scss';
</style>
mobile.vue
<!-- pages/login/components/mobile.vue -->
<script setup></script>
<template>
<uni-forms class="login-form" ref="form">
<uni-forms-item name="name">
<uni-easyinput
:input-border="false"
:clearable="false"
placeholder="请输入手机号"
placeholder-style="color: #C3C3C5"
/>
</uni-forms-item>
<uni-forms-item name="name">
<uni-easyinput
:input-border="false"
:clearable="false"
placeholder="请输入验证码"
placeholder-style="color: #C3C3C5"
/>
<text class="text-button">获取验证码</text>
</uni-forms-item>
<view class="agreement">
<radio :checked="false" color="#16C2A3" />
我已同意
<text class="link">用户协议</text>
及
<text class="link">隐私协议</text>
</view>
<button class="uni-button">登 录</button>
</uni-forms>
</template>
<script>
export default {
options: {
styleIsolation: 'shared',
},
}
</script>
<style lang="scss">
@import './styles.scss';
</style>
公共的样式 styles.scss
// pages/login/components/styles.scss
.uni-forms-item {
height: 80rpx;
margin-bottom: 30rpx !important;
border-bottom: 1rpx solid #ededed;
box-sizing: border-box;
position: relative;
}
.agreement {
font-size: 26rpx;
color: #3c3e42;
display: flex;
align-items: center;
margin-top: 50rpx;
margin-left: -10rpx;
.link {
color: #16c2a3;
}
:deep(.uni-radio-wrapper) {
transform: scale(0.6);
}
/* #ifdef MP */
radio {
transform: scale(0.6);
}
/* #endif */
:deep(.uni-radio-input) {
margin-right: 0 !important;
}
}
:deep(.uniui-eye-filled),
:deep(.uniui-eye-slash-filled) {
color: #6f6f6f !important;
}
:deep(.uni-forms-item__content) {
display: flex;
align-items: center;
}
:deep(.uni-forms-item__error) {
width: 100%;
padding-top: 10rpx;
padding-left: 10rpx;
border-top: 2rpx solid #eb5757;
color: #eb5757;
font-size: 24rpx;
transition: none;
}
.text-button {
display: flex;
justify-content: flex-end;
width: 240rpx;
padding-left: 10rpx;
font-size: 28rpx;
color: #16c2a3;
border-left: 2rpx solid #eee;
}
.uni-button {
margin-top: 50rpx;
&[disabled] {
background-color: #fafafa;
color: #d9dbde;
}
}
.uni-navigator {
margin-top: 30rpx;
text-align: center;
color: #848484;
font-size: 28rpx;
}
最后将组件导入到页面中,根据索引值来渲染相应的组件:
<!-- pages/login/index.vue -->
<script setup>
import { ref, computed } from 'vue'
// 导入组件
import customPassword from './components/password.vue'
import customMobile from './components/mobile.vue'
// 标签页要展示的内容
const tabMetas = [
{ title: '密码登录', subTitle: '验证码登录' },
{ title: '验证码登录', subTitle: '密码登录' },
]
// 标签页的索引值
const tabIndex = ref(1)
// 根据索引值决定当前标签展示的内容
const tabMeta = computed(() => {
return tabMetas[tabIndex.value]
})
// 切换标签页的索引值
function onSubTitleClick() {
// 0 和 1 互换的简单算法
tabIndex.value = Math.abs(tabIndex.value - 1)
}
</script>
<template>
<view class="user-login">
<view class="login-type">
<view class="title">{{ tabMeta.title }}</view>
<view class="type">
<text @click="onSubTitleClick">{{ tabMeta.subTitle }}</text>
<uni-icons color="#3c3e42" type="forward" />
</view>
</view>
<!-- 用户名&密码方式 -->
<custom-password v-if="tabIndex === 0" />
<!-- 短信验证码方式 -->
<custom-mobile v-if="tabIndex === 1" />
</view>
<!-- 社交账号登录 -->
<view class="social-login">
...
</view>
</template>
3.2 短信验证码登录
短信息验证码登录的大致流程如下:
- 用户填写正确的手机号码
- 向用户的手机号发送短信
- 用户填写接收到的短信验证码
- 同时提交验证码和手机号
============================================
- 提供了100个测试账号
- 手机号:13230000001 - 13230000100
- 密码:abc12345
============================================
3.2.1 倒计时组件
在获取短信码的过程中常常会配合倒计时的交互,提醒用户在 60秒可重新获取验证码,该交互可以使扩展组件 uni-countdown
,但是这个组件存在一些缺陷,我们将其改造后再使用:
-
将
uni_modules/uni-countdown/components/uni-countdown
内的全部内容拷贝到/components/custom-countdown
目录中 -
将
uni-countdown.vue
重命名为costom-countdown.vue
(目的是要符合 easycom 规范)
- 扩展(修改)组件
<!-- pages/login/components/mobile.vue -->
<script setup>
import { ref } from 'vue'
// 是否显示倒时计组件
const showCountdown = ref(false)
// 按钮文件
const buttonText = ref('获取验证码')
// 发送短信验证码
function onTextButtonClick() {
// 将来这里调用接口,发送短信...
// 显示倒计时组件
showCountdown.value = true
}
</script>
<template>
<uni-forms class="login-form" ref="form">
<uni-forms-item name="name">
...
</uni-forms-item>
<uni-forms-item name="name">
...
<view v-if="showCountdown" class="text-button">
<custom-countdown
:second="60"
:show-day="false"
color="#16C2A3"
/>
</view>
<text v-else @click="onTextButtonClick" class="text-button">
{{ buttonText }}
</text>
</uni-forms-item>
<view class="agreement">
...
</view>
<button class="uni-button">登 录</button>
</uni-forms>
</template>
为组件添加3个属性来控制是否显示 “时、分”,showHour
、showMiniute
<!-- /components/custom-countdown/custom-countdown.vue -->
<template>
<view class="uni-countdown">
<text v-if="showDay" :style="[timeStyle]" class="uni-countdown__number">
{{ d }}
</text>
<text v-if="showDay" :style="[splitorStyle]" class="uni-countdown__splitor">
{{ dayText }}
</text>
<text v-if="showHour" :style="[timeStyle]" class="uni-countdown__number">
{{ h }}
</text>
<text
v-if="showHour"
:style="[splitorStyle]"
class="uni-countdown__splitor"
>
{{ showColon ? ':' : hourText }}
</text>
<text v-if="showMiniute" :style="[timeStyle]" class="uni-countdown__number">
{{ i }}
</text>
<text
v-if="showMiniute"
:style="[splitorStyle]"
class="uni-countdown__splitor"
>
{{ showColon ? ':' : minuteText }}
</text>
<text :style="[timeStyle]" class="uni-countdown__number">{{ s }}</text>
<text
v-if="!showColon"
:style="[splitorStyle]"
class="uni-countdown__splitor"
>
{{ secondText }}
</text>
</view>
</template>
<script>
import { initVueI18n } from '@dcloudio/uni-i18n'
import messages from './i18n/index.js'
const { t } = initVueI18n(messages)
export default {
name: 'UniCountdown',
emits: ['timeup'],
props: {
showDay: {
type: Boolean,
default: true,
},
// *********
showHour: {
type: Boolean,
default: true,
},
showMiniute: {
type: Boolean,
default: true,
},
// *********
},
}
</script>
<style lang="scss" scoped>
...
</style>
监听custom-countdown
组件的事件 @timeup
,在倒时结束时允许用户重新获取验证码:
<!-- pages/login/components/mobile.vue -->
<script setup>
import { ref } from 'vue'
// 是否显示倒时计组件
const showCountdown = ref(false)
// 按钮文件
const buttonText = ref('获取验证码')
// 监听倒计时组件是否结束
function onCountdownTimeup() {
// 变更提示文字
buttonText.value = '重新获取验证码'
// 隐藏倒计时组件
showCountdown.value = false
}
// 发送短信验证码
function onTextButtonClick() {
// 将来这里调用接口,发送短信...
// 显示倒计时组件
showCountdown.value = true
}
</script>
<template>
<uni-forms class="login-form" ref="form">
<uni-forms-item name="name">
...
</uni-forms-item>
<uni-forms-item name="name">
...
<view v-if="showCountdown" class="text-button">
<custom-countdown
:second="59"
:show-day="false"
:show-hour="false"
:show-miniute="false"
@timeup="onCountdownTimeup"
color="#16C2A3"
/>
</view>
<text v-else @click="onTextButtonClick" class="text-button">
{{ buttonText }}
</text>
</uni-forms-item>
<view class="agreement">
...
</view>
<button class="uni-button">登 录</button>
</uni-forms>
</template>
注意事项:以上的倒时计组件显示时间时,如果设置为 60秒时,会被处理成 ‘01:00’,因此看到的秒数是 ‘00’,而不是 ‘60’,这个小瑕疵我们可以将时间置成 59秒,偷懒的方式解决这个问题。
3.2.2 表单数据验证
要验证表单的数据是否合法,需要3个步骤:
- 获取表单的数据
<!-- pages/login/components/mobile.vue -->
<script setup>
import { ref } from 'vue'
// 省略前面小节代码...
// 表单数据
const formData = ref({
mobile: '',
code: '',
})
// 省略前面小节代码
</script>
<template>
<uni-forms class="login-form" :model="formData" ref="form">
<uni-forms-item name="mobile">
<uni-easyinput
v-model="formData.mobile"
:input-border="false"
:clearable="false"
placeholder="请输入手机号"
placeholder-style="color: #C3C3C5"
/>
</uni-forms-item>
<uni-forms-item name="code">
<uni-easyinput
v-model="formData.code"
:input-border="false"
:clearable="false"
placeholder="请输入验证码"
placeholder-style="color: #C3C3C5"
/>
<view v-if="showCountdown" class="text-button">
<custom-countdown
:second="60"
:show-day="false"
:show-hour="false"
:show-miniute="false"
@timeup="onCountdownTimeup"
color="#16C2A3"
/>
秒后重新获取
</view>
<text v-else @click="onTextButtonClick" class="text-button">
{{ buttonText }}
</text>
</uni-forms-item>
<view class="agreement">
...
</view>
<button class="uni-button">登 录</button>
</uni-forms>
</template>
注意事项,以上代码中关键的部分为:
-
给
uni-forms
组件添加:model
属性 -
给
uni-forms-item
组件添加name
属性 -
给
uni-easyinput
组件添加v-model
属性
- 定义验证规则
<!-- pages/login/components/mobile.vue -->
<script setup>
import { ref } from 'vue'
// 省略前面小节代码...
// 表单数据
const formData = ref({
mobile: '',
code: '',
})
// 验证表单数据的规则
const formRules = {
mobile: {
rules: [
{ required: true, errorMessage: '请填写手机号码' },
{ pattern: '^1\\d{10}$', errorMessage: '手机号码格式不正确' },
],
},
code: {
rules: [
{ required: true, errorMessage: '请输入验证码' },
{ pattern: '^\\d{6}$', errorMessage: '验证码格式不正确' },
],
},
}
// 省略前面小节代码
</script>
<template>
<uni-forms class="login-form" :model="formData" :rules="formRules" ref="formRef">
...
</uni-forms>
</template>
注意事项,以上代码中关键部分为:
- 给
uni-forms
组件添加了:rules
属性 - 定义验证规则时,验证规要与
uni-forms-item
的name
属性相对应
- 调用验证方法
<!-- pages/login/components/mobile.vue -->
<script setup>
import { ref } from 'vue'
// 省略前面小节代码...
// 表单数据
const formData = ref({
mobile: '',
code: '',
})
// 验证表单数据的规则
const formRules = {
mobile: {
rules: [
{ required: true, errorMessage: '请填写手机号码' },
{ pattern: '^1\\d{10}$', errorMessage: '手机号码格式不正确' },
],
},
code: {
rules: [
{ required: true, errorMessage: '请输入验证码' },
{ pattern: '^\\d{6}$', errorMessage: '验证码格式不正确' },
],
},
}
// 提交表单数据
async function onFormSubmit() {
// 调用 uniForms 组件验证数据的方法
try {
// 验证通过后会返回表单的数据
const formData = await formRef.value.validate()
} catch (error) {
console.log(error)
}
}
// 省略前面小节代码
</script>
<template>
<uni-forms class="login-form" :model="formData" :rules="formRules" ref="formRef">
...
<button @click="onFormSubmit" class="uni-button">登 录</button>
</uni-forms>
</template>
3.3.3 调用接口
将表单的数据发送给服务端接口,分成 2 个步骤来实现:
- 封装接口调用的方法,接口文档的地址查看这里,同时修改接口的基地址和自定义请求头
Authorization
// utils/http.js
// 导入模块
import Request from 'luch-request'
// 实例化网络请求
const http = new Request({
// 接口基地址
baseURL: 'https://consult-api.itheima.net/',
custom: {
loading: true,
},
})
// 请求拦截器
http.interceptors.request.use(
function (config) {
// 显示加载状态提示
if (config.custom.loading) {
uni.showLoading({ title: '正在加载...', mask: true })
}
config.header = {
// Authorization: '22222222',
...config.header,
}
return config
},
function (error) {
return Promise.reject(error)
}
)
// 省略前面小节代码...
将接口调用的方法进行统一的管理,放到 services
目录中,然后分模块来对接口的调用进行封装。
// services/user.js
// 导入封装好的网络请求模块
import { http } from '@/utils/http'
/**
* 发送验证码
*/
export const verifyCodeApi = (data) => {
// get 方法的参数需要通过 params 来传递
return http.get('/code', { params: data })
}
/**
* 用户登录接口(短信验证码方式)
*/
export const loginByMobileApi = (data) => {
return http.post('/login', data)
}
注意事项,上述代码中将 Api
做为方法名的后缀,如 loginByMobileApi
,目的是方便代码的阅读,一目了然的知道是对接口调用进行的封装。
HBuilder X 使用小技巧:在代码中直接写封装好的 API 方法,根据提示可以快速引用相应的文件模块。
先来调用接口获取短信验证码
<!-- pages/login/components/mobile.vue -->
<script setup>
import { ref } from 'vue'
import { verifyCodeApi } from '@/services/user'
// 省略前面小节的代码
// 提交表单数据
async function onFormSubmit() {
// 调用 uniForms 组件验证数据的方法
try {
// 验证通过后会返回表单的数据
const formData = await formRef.value.validate()
} catch (error) {
console.log(error)
}
}
// 省略前面小节的代码...
// 发送短信验证码
async function onTextButtonClick() {
// 将来这里调用接口,发送短信...
const { code, message } = await verifyCodeApi({
mobile: formData.value.mobile,
type: 'login',
})
// 检测接口是否调用成功
if (code !== 10000) return uni.utils.toast(message)
uni.utils.toast('验证码已发送,请查收!')
// 显示倒计时组件
showCountdown.value = true
}
</script>
接收到短信验证码之后再来将表单的全部数据 mobile
和 code
提交给接口
<!-- pages/login/components/mobile.vue -->
<script setup>
import { ref } from 'vue'
import { loginByMobileApi, verifyCodeApi } from '@/services/user'
// 省略前面小节的代码
// 提交表单数据
async function onFormSubmit() {
// 调用 uniForms 组件验证数据的方法
try {
// 验证通过后会返回表单的数据
const formData = await formRef.value.validate()
// 提交表单数据
const { code, data, message } = await loginByMobileApi(formData)
// 检测接口是否调用成功
if (code !== 10000) return uni.utils.toast(message)
} catch (error) {
console.log(error)
}
}
// 省略前面小节的代码...
</script>
- 记录用户登录状态,通过 Pinia 将登录状态记录下来
新建用于管理用户数据的 Store,通过 token 来记录用户的登录状态:
// stores/user.js
import { ref } from 'vue'
import { defineStore } from 'pinia'
export const useUserStore = defineStore(
'user',
() => {
// 记录用户登录状态
const token = ref('')
return { token }
},
{
persist: {
// 指定需要持久化的数据
paths: ['token'],
},
}
)
接下来在登录成功后来更新 Pinia 中的 token
<!-- pages/login/components/mobile.vue -->
<script setup>
import { ref } from 'vue'
import { loginByMobileApi, verifyCodeApi } from '@/services/user'
import { useUserStore } from '@/stores/user'
// 用户相关的数据
const userStore = useUserStore()
// 省略前面小节的代码
// 提交表单数据
async function onFormSubmit() {
// 调用 uniForms 组件验证数据的方法
try {
// 验证通过后会返回表单的数据
const formData = await formRef.value.validate()
// 提交表单数据
const { code, data, message } = await loginByMobileApi(formData)
// 检测接口是否调用成功
if (code !== 10000) return uni.utils.toast(message)
// 持久化存储 token
userStore.token = data.token
// 临时跳转到首页面
uni.switchTab({
url: '/pages/index/index',
})
} catch (error) {
console.log(error)
}
}
// 省略前面小节的代码...
</script>
四、作业
4.1 部分表单验证
uniForms 提供的 validate
方法来验证整个表单的数据,还提供了 validateField
方法来验证部分表单数据,其语法如下:
// uniForms 表单组件
const formRef = ref()
// 传入一个数组,数组中每个单元即要验证的数据名称,返回值为 Promise
formRef.value.validateField(['mobile', '数据2', '数据3'])
知道 validateField
的用法后,将来整合到项目中
<script setup>
import {ref} from 'vue'
// uniForms 表单组件
const formRef = ref()
// 省略前面小节的代码...
// 发送短信验证码
async function onTextButtonClick() {
try {
// 验证表单数据(手机号)
await formRef.value.validateField(['mobile'])
// 将来这里调用接口,发送短信...
const { code, message } = await verifyCodeApi({
mobile: formData.value.mobile,
type: 'login',
})
// 检测接口是否调用成功
if (code !== 10000) return uni.utils.toast(message)
uni.utils.toast('验证码已发送,请查收!')
// 显示倒计时组件
showCountdown.value = true
} catch (error) {
console.log(error)
}
}
</script>
4.2 是否同意协议
监听 checkbox
组件的单击事件,变更组件的 checked
属性,true
为选中,false
为不选中
<!-- pages/login/components/mobile.vue -->
<script setup>
import { ref } from 'vue'
import { loginByMobileApi, verifyCodeApi } from '@/services/user'
import { useUserStore } from '@/stores/user'
// 用户相关的数据
const userStore = useUserStore()
// 是否同意协议
const isAgree = ref(false)
// 省略前面小节代码...
// 是否同意协议
function onAgreeClick() {
isAgree.value = !isAgree.value
}
// 省略前面小节代码...
</script>
<template>
<uni-forms
class="login-form"
:model="formData"
:rules="formRules"
ref="formRef"
>
...
<view class="agreement">
<radio @click="onAgreeClick" :checked="isAgree" color="#16C2A3" />
我已同意
<text class="link">用户协议</text>
及
<text class="link">隐私协议</text>
</view>
<button @click="onFormSubmit" class="uni-button">登 录</button>
</uni-forms>
</template>
在点击表单提交按钮后判断 isAgree
的值是否为 true
<!-- pages/login/components/mobile.vue -->
<script setup>
import { ref } from 'vue'
import { loginByMobileApi, verifyCodeApi } from '@/services/user'
import { useUserStore } from '@/stores/user'
// 用户相关的数据
const userStore = useUserStore()
// 是否同意协议
const isAgree = ref(false)
// 省略前面小节代码...
// 提交表单数据
async function onFormSubmit() {
// 判断是否勾选协议
if (!isAgree.value) return uni.utils.toast('请先同意协议!')
// 省略前面小节代码...
}
// 是否同意协议
function onAgreeClick() {
isAgree.value = !isAgree.value
}
// 省略前面小节代码...
</script>
<template>
<uni-forms
class="login-form"
:model="formData"
:rules="formRules"
ref="formRef"
>
...
<view class="agreement">
<radio @click="onAgreeClick" :checked="isAgree" color="#16C2A3" />
我已同意
<text class="link">用户协议</text>
及
<text class="link">隐私协议</text>
</view>
<button @click="onFormSubmit" class="uni-button">登 录</button>
</uni-forms>
</template>
4.3 用户名和密码登录
参考代码:
// services/user.js
// 导入封装好的网络请求模块
import { http } from '@/utils/http'
/**
* 发送验证码
*/
export const verifyCodeApi = (data) => {
return http.get('/code', { params: data })
}
/**
* 用户登录接口(短信验证码方式)
*/
export const loginByMobileApi = (data) => {
return http.post('/login', data)
}
/**
* 用户登录接口(密码方式)
*/
export const loginByPassword = (data) => {
return http.post('/login/password', data)
}
<!-- pages/login/components/password.vue -->
<script setup>
import { ref } from 'vue'
import { loginByPassword } from '@/services/user'
import { useUserStore } from '@/stores/user'
// 用户相关的数据
const userStore = useUserStore()
// 是否同意协议
const isAgree = ref(false)
// 获取表单组件
const formRef = ref()
// 表单数据
const formData = ref({
mobile: '',
password: '',
})
// 验证表单数据的规则
const formRules = {
mobile: {
rules: [
{ required: true, errorMessage: '请填写手机号码' },
{ pattern: '^1\\d{10}$', errorMessage: '手机号码格式不正确' },
],
},
password: {
rules: [
{ required: true, errorMessage: '请输入验证码' },
{ pattern: '^[a-zA-Z0-9]{8}$', errorMessage: '密码格式不正确' },
],
},
}
// 是否同意协议
function onAgreeClick() {
isAgree.value = !isAgree.value
}
// 提交表单数据
async function onFormSubmit() {
// 判断是否勾选协议
if (!isAgree.value) return uni.utils.toast('请先同意协议!')
// 调用 uniForms 组件验证数据的方法
try {
// 验证通过后会返回表单的数据
const formData = await formRef.value.validate()
// 提交表单数据
const { code, data, message } = await loginByPassword(formData)
// 检测接口是否调用成功
if (code !== 10000) return uni.utils.toast(message)
// 持久化存储 token
userStore.token = data.token
// 临时跳转到首页面
uni.switchTab({
url: '/pages/index/index',
})
} catch (error) {
console.log(error)
}
}
</script>
<template>
<uni-forms
class="login-form"
:model="formData"
:rules="formRules"
ref="formRef"
>
<uni-forms-item name="mobile">
<uni-easyinput
v-model="formData.mobile"
:input-border="false"
:clearable="false"
placeholder="请输入手机号"
placeholder-style="color: #C3C3C5"
/>
</uni-forms-item>
<uni-forms-item name="password">
<uni-easyinput
v-model="formData.password"
type="password"
placeholder="请输入密码"
:input-border="false"
placeholder-style="color: #C3C3C5"
/>
</uni-forms-item>
<view class="agreement">
<radio @click="onAgreeClick" :checked="isAgree" color="#16C2A3" />
我已同意
<text class="link">用户协议</text>
及
<text class="link">隐私协议</text>
</view>
<button @click="onFormSubmit" class="uni-button">登 录</button>
<navigator hover-class="none" class="uni-navigator" url=" ">
忘记密码?
</navigator>
</uni-forms>
</template>
<script>
export default {
options: {
styleIsolation: 'shared',
},
}
</script>
<style lang="scss">
@import './styles.scss';
</style>