Vue项目实战——基于 Vue3.x + Vant UI实现一个多功能记账本(登录注册页面,验证码)

Posted 前端杂货铺

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue项目实战——基于 Vue3.x + Vant UI实现一个多功能记账本(登录注册页面,验证码)相关的知识,希望对你有一定的参考价值。

基于 Vue3.x + Vant UI 的多功能记账本(四)


文章目录

系列内容参考链接
基于 Vue3.x + Vant UI 的多功能记账本(一)项目演示,涉及知识点
基于 Vue3.x + Vant UI 的多功能记账本(二)搭建开发环境
基于 Vue3.x + Vant UI 的多功能记账本(三)开发导航栏及公共部分

项目演示

Vue3 + Vant UI_多功能记账本

1、登录注册页面

页面设计,页面跳转

Login.vue

<template>
  <!-- 根据页面显示相应头部 -->
  <Header :title="type == 'login' ? '登录' : '注册'" />
  <div class="auth">
    <img class="logo" src="//s.yezgea02.com/1606836859539/onpeice.png" alt="" />
    <!-- 登录界面的表单 -->
    <van-form class="form-wrap" @submit="onSubmit" v-if="type == 'login'">
      <div class="form">
        <!-- 账号输入框,clearable:清除图标,rules:表单校验规则 -->
        <van-field
          clearable
          v-model="username"
          name="username"
          label="账号"
          placeholder="请输入账号"
          :rules="[ required: true, message: '请填写账户' ]"
        />
        <!-- 密码输入框 -->
        <van-field
          clearable
          v-model="password"
          type="password"
          name="password"
          label="密码"
          placeholder="请输入密码"
          :rules="[ required: true, message: '请填写密码' ]"
        />
      </div>
      <div style="margin: 16px 0">
        <van-button round block type="primary" native-type="submit">
          登录
        </van-button>
        <p @click="chanegType('register')" class="change-btn">
          没有账号,前往注册
        </p>
      </div>
    </van-form>
    <!-- 注册页面的表单 -->
    <van-form class="form-wrap" @submit="onSubmit" v-if="type == 'register'">
      <div class="form">
        <van-field
          clearable
          v-model="username"
          name="username"
          label="账号"
          placeholder="请输入账号"
          :rules="[ required: true, message: '请填写账号' ]"
        />
        <van-field
          clearable
          v-model="password"
          type="password"
          name="password"
          label="密码"
          placeholder="请输入密码"
          :rules="[ required: true, message: '请填写密码' ]"
        />
        <!-- 验证码输入框 -->
        <van-field
          center
          clearable
          label="验证码"
          placeholder="输入验证码"
          v-model="verify"
        >
          <!-- 点击刷新验证码 -->
          <template #button>
            <!-- 生成验证码图片组件,ref 方便拿到组件内的实例属性 -->
            <VueImgVerify ref="verifyRef" />
          </template>
        </van-field>
      </div>
      <div style="margin: 16px 0">
        <van-button round block type="primary" native-type="submit">
          注册
        </van-button>
        <p @click="chanegType('login')" class="change-btn">登录已有账号</p>
      </div>
    </van-form>
  </div>
</template>

<script>
import  reactive, toRefs, ref, onMounted  from "vue";
// 生成验证码的组件
import VueImgVerify from "../components/VueImageVerify.vue";
import Header from "../components/Header.vue";
import axios from "../utils/axios";
// 轻提示(成功/失败...)
import  Toast  from "vant";
import router from "../router";
export default 
  name: "Login",
  components: 
    VueImgVerify, // 验证码组件
    Header, //公共头组件
  ,
  setup() 
    // 便于拿到 verifyRef 组件内的实例属性
    const verifyRef = ref(null);
    // 注册登录的相关内容
    const state = reactive(
      username: "",
      password: "",
      type: "login", // 登录注册模式切换参数
      verify: "", // 验证码输入框输入的内容
      imgCode: "", // 生成的验证图片内的文字
    );

    console.log("verifyRef", verifyRef);
    // 提交登录 or 注册表单
    const onSubmit = async (values) => 
      // 登录功能
      if (state.type == "login") 
        const  data  = await axios.post("/user/login", 
          username: state.username,
          password: state.password,
        );
        // 添加 token 到本地存储
        localStorage.setItem("token", data.token);
        window.location.href = "/";
       else 
        // 生成的图片验证码的文字等于验证码组件生成的验证码
        state.imgCode = verifyRef.value.imgCode || "";
        // 如果验证码组件生成的验证码的小写 != 用户输入的验证码的小写,则提示错误
        if (
          verifyRef.value.imgCode.toLowerCase() != state.verify.toLowerCase()
        ) 
          console.log("verifyRef.value.imgCode", verifyRef.value.imgCode);
          Toast.fail("验证码错误");
          return;
        
        // 验证码匹配成功,注册=>注册成功
        await axios.post("/user/register", 
          username: state.username,
          password: state.password,
        );
        Toast.success("注册成功");
      
    ;

    // 切换登录和注册两种模式
    const chanegType = (type) => 
      state.type = type;
    ;

    return 
      ...toRefs(state),
      onSubmit,
      chanegType,
      verifyRef,
    ;
  ,
;
</script>

<style lang='less' scoped>
@import url("../config/custom.less");
.auth 
  height: calc(~"(100% - 46px)");
  padding: 30px 20px 0 20px;
  background: @primary-bg;
  .logo 
    width: 150px;
    display: block;
    margin: 0 auto;
    margin-bottom: 30px;
  
  .form-wrap 
    .form 
      border-radius: 10px;
      overflow: hidden;
      .van-cell:first-child 
        padding-top: 20px;
      
      .van-cell:last-child 
        padding-bottom: 20px;
      
    
  
  .change-btn 
    text-align: center;
    margin: 10px 0;
    color: @link-color;
    font-size: 14px;
  

</style>

在 custom.less 下补充 link-color 变量的定义,在写样式的时候,以 color: @link-color; 这样的形式引用它

custom.less

@primary: #39be77; // 主题色
@danger: #fc3c0c; 
@primary-bg: #f5f5f5;
@link-color: #597fe7;

当前页面的外层是 #app、body,作为父级,它们需要先把高度撑开

index.css

body,
html,
p 
  height: 100%;
  margin: 0;
  padding: 0;


* 
  box-sizing: border-box;


#app 
  height: 100%;

此时,yarn dev,打开浏览器可以看到…

2、图片验证码

注:验证码基本上都是由服务端接口提供,然后上报之后由服务端验证是否正确,所以此部分内容可以自行选择是否去做。

<template>
  <div class="img-verify">
    <!-- 画布,绑定一个点击事件,用于刷新验证码 -->
    <canvas
      ref="verify"
      :width="width"
      :height="height"
      @click="handleDraw"
    ></canvas>
  </div>
</template>
<script type="text/ecmascript-6">
import  reactive, onMounted, ref, toRefs  from "vue";
export default 
  setup() 
    const verify = ref(null);
    const state = reactive(
      pool: "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", // 字符串
      width: 120,
      height: 40,
      imgCode: "", // 初始化验证码为空
    );
    onMounted(() => 
      // 初始化绘制图片验证码
      state.imgCode = draw();
    );

    // 点击图片重新绘制
    const handleDraw = () => 
      state.imgCode = draw();
    ;

    // 随机数
    const randomNum = (min, max) => 
      return parseInt(Math.random() * (max - min) + min);
    ;
    // 随机颜色
    const randomColor = (min, max) => 
      const r = randomNum(min, max);
      const g = randomNum(min, max);
      const b = randomNum(min, max);
      return `rgb($r,$g,$b)`;
    ;

    // 绘制图片
    const draw = () => 
      // 3.填充背景颜色,背景颜色要浅一点
      const ctx = verify.value.getContext("2d");
      // 填充颜色
      ctx.fillStyle = randomColor(180, 230);
      // 填充的位置
      ctx.fillRect(0, 0, state.width, state.height);
      // 定义paramText
      let imgCode = "";
      // 4.随机产生字符串,并且随机旋转
      for (let i = 0; i < 4; i++) 
        // 随机的四个字
        const text = state.pool[randomNum(0, state.pool.length)];
        imgCode += text;
        // 随机的字体大小
        const fontSize = randomNum(18, 40);
        // 字体随机的旋转角度
        const deg = randomNum(-30, 30);
        /*
         * 绘制文字并让四个文字在不同的位置显示的思路 :
         * 1、定义字体
         * 2、定义对齐方式
         * 3、填充不同的颜色
         * 4、保存当前的状态(以防止以上的状态受影响)
         * 5、平移 translate()
         * 6、旋转 rotate()
         * 7、填充文字
         * 8、restore 出栈
         * */
        ctx.font = fontSize + "px Simhei";
        ctx.textBaseline = "top";
        ctx.fillStyle = randomColor(80, 150);
        /*
         * save() 方法把当前状态的一份拷贝压入到一个保存图像状态的栈中。
         * 这就允许您临时地改变图像状态,
         * 然后,通过调用 restore() 来恢复以前的值。
         * save是入栈,restore 是出栈。
         * 用来保存Canvas的状态。save 之后,可以调用 Canvas 的平移、放缩、旋转、错切、裁剪等操作。 restore:用来恢复 Canvas 之前保存的状态。防止 save 后对 Canvas 执行的操作对后续的绘制有影响。
         *
         * */
        ctx.save();
        ctx.translate(30 * i + 15, 15);
        ctx.rotate((deg * Math.PI) / 180);
        // fillText() 方法在画布上绘制填色的文本。文本的默认颜色是黑色。
        // 请使用 font 属性来定义字体和字号,并使用 fillStyle 属性以另一种颜色/渐变来渲染文本。
        // context.fillText(text,x,y,maxWidth);
        ctx.fillText(text, -15 + 5, -15);
        ctx.restore();
      
      // 5.随机产生5条干扰线,干扰线的颜色要浅一点
      for (let i = 0; i < 5; i++) 
        ctx.beginPath();
        ctx.moveTo(randomNum(0, state.width), randomNum(0, state.height));
        ctx.lineTo(randomNum(0, state.width), randomNum(0, state.height));
        ctx.strokeStyle = randomColor(180, 230);
        ctx.closePath();
        ctx.stroke();
      
      // 6.随机产生40个干扰的小点
      for (let i = 0; i < 40; i++) 
        ctx.beginPath();
        ctx.arc(
          randomNum(0, state.width),
          randomNum(0, state.height),
          1,
          0,
          2 * Math.PI
        );
        ctx.closePath();
        ctx.fillStyle = randomColor(150, 200);
        ctx.fill();
      
      return imgCode;
    ;

    return 
      ...toRefs(state),
      verify,
      handleDraw,
    ;
  ,
;
</script>
<style type="text/css">
.img-verify canvas 
  cursor: pointer;

</style>

此时,yarn dev,打开浏览器可以看到…

3、修改 axios

为避免在页面内请求接口的时候,每次都通过 code 码去判断接口请求是否成功,我们可以这样修改 axios.js 文件

axios.js

import axios from 'axios'
// 轻提示插件(Vant UI)
import  Toast  from 'vant'
import router from '../router'

// 根据环境变量切换本地和线上的请求地址
axios.defaults.baseURL = process.env.NODE_ENV == 'development' ? '/api' : '//47.99.134.126:7008/api'
// 允许跨域
axios.defaults.withCredentials = true
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
// token的用户鉴权方式,在请求头的 headers 内添加 token,每次请求都会验证用户信息
axios.defaults.headers['Authorization'] = `$localStorage.getItem('token') || null`
axios.defaults.headers.post['Content-Type'] = 'application/json'

axios.interceptors.response.use(res => 
  // 返回数据的类型不是对象,则报异常
  if (typeof res.data !== 'object') 
    Toast.fail('服务端异常!')
    return Promise.reject(res)
  
  // code 状态码不是200,则报异常
  if (res.data.code != Vue3.x 项目实战(一) 
项目名参考链接
Vue2.x_todoList基于 Vue2.x 实现一个任务清单
Vue2.x_GitHub搜素案例基于 Vue2.x GitHub 搜素案例

文章目录


Vue3.x 实现 todoList

1、前言

如果你对 vue3 的基础知识还很陌生,推荐先去学习一下 vue 基础

内容参考链接
Vue2.x全家桶Vue2.x全家桶参考链接
Vue3.x的基本使用Vue3.x基本使用参考链接
  • 如果你 刚学完 vue3 基础知识,想检查一下自己的学习成果
  • 如果你 已学完 vue3 基础知识,想快速回顾复习
  • 如果你 已精通 vue3 基础知识,想做个小案例
  • 那不妨看完这篇文章,我保证你一定会有收获的!

2、项目演示(一睹为快)

Vue3.x_任务清单

3、涉及知识点

麻雀虽小,五脏俱全,接下来开始我们的项目之旅吧~~

  • Vue3.x基础:插值语法,常用指令,键盘事件,列表渲染,计算属性,生命周期
  • Vue3.x进阶:props(父传子),自定义事件(任意组件间通信)
  • Vuex4.x:状态管理库的使用
  • Vue-router4.x:使用路由进行页面跳转

备注:

  1. 任意组件间的通信方式有很多种(全局事件总线,消息订阅预发布…),熟练掌握一种即可(推荐自定义事件,配置简单,容易理解)
  2. 本文是 vue 基础的练习项目,也涉及 vue 周边(Vuex,Vue-Router)

4、项目详情

main.js

  • 导入 store 和 router,并且使用
import  createApp  from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

createApp(App).use(store).use(router).mount('#app')

./router/index.js

  • 配置路由
  • 直接导入 VS 按需导入(节约性能)
  • 使用了 history 路由模式
import  createRouter, createWebHistory  from 'vue-router'
// 直接引入
import Start from '../views/Start.vue'

const routes = [
  
    path: '/',
    name: 'Start',
    component: Start
  ,
  
    path: '/home',
    name: 'Home',
    // 按需引入,节约性能
    component: () => import('../views/Home.vue')
  
]

// 创建路由对象
const router = createRouter(
  history: createWebHistory(process.env.BASE_URL),
  routes
)

export default router

./store/index.js

  • state 中定义初始化数据
  • mutations 中定义方法
import 
  createStore
 from 'vuex'

export default createStore(
  // 定义初始化状态
  state: 
    list: [
      
        title: "吃饭",
        complete: false,
      ,
      
        title: "睡觉",
        complete: false,
      ,
      
        title: "敲代码",
        complete: true,
      ,
    ]
  ,
  
  // 同步修改 state 都是方法
  // 第一个参数 state 第二个参数是需要修改的值
  mutations: 
    // 添加任务
    addTodo(state, payload) 
      state.list.push(payload)
    ,
    // 删除任务
    delTodo(state, payload) 
      state.list.splice(payload, 1)
    ,
    // 清除已完成
    clear(state, payload) 
      // 把过滤之后的数组传进来
      state.list = payload
    
  ,
  // 异步提交 mutation
  // 第一个参数是 store 第二个参数是修改的值
  actions: 

  ,
  // 模块化
  modules: 
)

App.vue 组件

  • 做呈现的组件
  • <router-view /> 呈现内容
<template>
  <router-view/>
</template>

<style lang="scss">
  * 
    margin: 0;
    padding: 0;
  
</style>

Start.vue 组件

  • 初始化页面
  • 点击开启任务,跳转到任务页面
<template>
  <div class="title">
    <h1>欢迎来到前端杂货铺</h1>
    <button @click="start">开始任务</button>
  </div>
</template>
  
<script>
import  ref  from "vue";
import  useRouter  from "vue-router";
export default 
  name: "Start",
  setup() 
    // router 是全局路由对象
    let router = useRouter();
    let name = ref(10);
    // 点击进行路由跳转
    let start = () => 
      router.push(
        name: "Home",
        params: 
          name: name.value,
        ,
      );
    ;
    return 
      start,
    ;
  ,
;
</script>
  
<style lang="scss" scoped>
.title 
  color: orange;
  text-align: center;
  margin-top: 20%;

button 
  margin-top: 20px;
  width: 100px;
  height: 50px;
  background: skyblue;
  color: white;
  font-weight: bold;
  font-size: 15px;
  cursor: pointer;

button:hover 
  font-weight: bold;
  background: white;
  color: skyblue;
  cursor: pointer;

</style>

Home.vue 组件

  • 其他组件 表演的舞台
  • 传递数据
  • 自定义事件,进行组件间通信
<template>
  <div class="container">
    <nav-header @add="add"></nav-header>
    <nav-main :list="list" @del="del"></nav-main>
    <nav-footer :list="list" @clear="clear"></nav-footer>
  </div>
</template>

<script>
import NavHeader from "@/components/navHeader/NavHeader";
import NavMain from "@/components/navMain/NavMain";
import NavFooter from "@/components/navFooter/NavFooter";
import  ref, computed  from "vue";
import  useStore  from "vuex";
export default 
  name: "Home",
  // 接收父组件的数据
  props: ,
  // 定义子组件
  components: 
    NavHeader,
    NavMain,
    NavFooter,
  ,

  // 接收的数据,上下文
  setup(props, ctx) 
    let store = useStore();
    let list = computed(() => 
      return store.state.list;
    );
    let value = ref("");
    // 添加任务
    let add = (val) => 
      value.value = val;
      // 任务存在 不能重复添加
      let flag = true;
      list.value.map((item) => 
        if (item.title === value.value) 
          // 有重复任务
          flag = false;
          alert("任务已存在");
        
      );
      // 没有重复任务
      if (flag == true) 
        // 调用 mutation
        store.commit("addTodo", 
          title: value.value,
          complete: false,
        );
      
    ;

    // 删除任务
    let del = (val) => 
      // 调用删除的 mutation
      store.commit('delTodo', val)
      console.log(val);
    

    // 清除已完成
    let clear = (val) => 
      store.commit('clear', val)
    
    
    return 
      add,
      list,
      del,
      clear
    ;
  ,
;
</script>

NavHeader.vue 组件

  • 头部组件(输入框)
  • 输入任务按下回车进行任务的添加
  • emit,使用分发的事务
<template>
  <div>
    <div class="container">
      <input
        type="text"
        placeholder="请输入任务名称"
        v-model="value"
        @keyup.enter="enter"
      />
    </div>
  </div>
</template>

<script>
import  ref  from "vue";
export default 
  name: "navHeader",
  // 接收的数据,上下文
  setup(props, ctx) 
    let value = ref("");
    // 按回车键确认
    let enter = () => 
      // 把输入框的内容传递给父组件
      ctx.emit("add", value.value);
      // 清空输入框
      value.value = "";
    ;
    return 
      value,
      enter,
    ;
  ,
;
</script>

<style lang="scss" scoped>
.container 
  text-align: center;
  margin-top: 220px;

.container input 
  height: 25px;
  width: 200px;
  margin-bottom: 10px;

</style>

NavMain.vue 组件

  • props 接收 list 数据
  • v-for 遍历出来内容
  • 使用条件判断做呈现
<template>
  <div class="container">
    <div v-if="list.length > 0">
      <div v-for="(item, index) in list" :key="index">
        <div class="item">
          <input type="checkbox" v-model="item.complete" />
           item.title 
          <button class="del" @click="del(item, index)">删除</button>
        </div>
      </div>
    </div>
    <div v-else>
      暂无任务
    </div>
  </div>
</template>

<script>
export default 
  name: "navMain",
  props: 
    list: 
      type: Array,
      required: true,
    ,
  ,
  // 分发事件的属性名
  emits: ["del"],
  setup(props, ctx) 
    // 删除任务
    let del = (item, index) => 
      ctx.emit("del", index);
      console.log(index, item);
    ;
    return 
      del,
    ;
  ,
;
</script>

<style lang="scss" scoped>
.container 
  margin: auto;
  border: 2px solid #ccc;
  width: 200px;
  margin-bottom: 20px;
  

.item 
  height: 35px;
  line-height: 35px;
  position: relative;
  width: 200px;
  button 
    cursor: pointer;
    position: absolute;
    right: 4px;
    top: 6px;
    display: none;
    z-index: 99;
  
  &:hover 
    background: #ddd;
    button 
      display: block;
    
  

</style>

NavFooter.vue 组件

  • 过滤出已完成的任务,获取到已完成任务的个数
  • 过滤出未完成的任务,清除的时候保留未完成的任务
<template>
  <div class="container">
      已完成  isCompelete  / 全部  list.length 
      <span v-if="isCompelete" class="btn">
        <button @click="clear">清除已完成</button>
      </span>
  </div>
</template>

<script>
import  computed  from "vue";
export default 
  name: "navFooter",
  props: 
    list: 
      type: Array,
      required: true,
    ,
  ,
  setup(props, ctx) 
    let isCompelete = computed(() => 
      // 过滤已完成
      let arr = props.list.filter((item) => 
        return item.complete;
      );
      return arr.length;
    );
    // 清除已完成
    let clear = () => 
      // 过滤未完成的
      let arr = props.list.filter((item) => 
        return item.complete === false;
      );
      ctx.emit("clear", arr);
      console.log("clear");
    ;

    return 
      isCompelete,
      clear,
    ;
  ,
;
</script>

<style lang="scss" scoped>
.container 
  text-align: center;

</style>

至此,此项目就实现了,如果什么问题,欢迎评论区或私信留言,看到定会第一时间解决!

5、写在最后的话

如果你是 看完全篇 阅读到了这里,我相信你一定是有收获的!

那么下面不妨打开自己的电脑,启动自己的编译器,来跟着做 / 自己做一遍吧!

有机会的话,在不久的将来还会对这个小案例进行升级(功能以及样式的升级)敬请期待吧~~

6、附源码

如果这篇文章对你有些许帮助的话,不妨 三连 + 关注 支持一下~~

下方微信小程序,回复【任务清单】即可获取源码

以上是关于Vue项目实战——基于 Vue3.x + Vant UI实现一个多功能记账本(登录注册页面,验证码)的主要内容,如果未能解决你的问题,请参考以下文章

Vue项目实战——实现一个任务清单基于 Vue3.x 全家桶(简易版)

三个小时vue3.x从零到实战(后)(vue3.x配套工具及项目化构建)

三个小时vue3.x从零到实战(vue3.x面试总结)

三个小时vue3.x从零到实战(前)(vue3.x基础)

三个小时vue3.x从零到实战(中)(vue3.x高级语法)

三个小时vue3.x从零到实战(vue3.x经典案例46个)