Vue3实现 饿了吗 笔记3——login.vue

登录界面原代码效果

原代码只实现了密码登录,手机登录没写完。


App.vue

写了登录界面,就要实现从首页跳转到登录界面(首页未实现)。需要用vue-router实现该效果。vue-router的router-view是写在App.vue上的,看了一下源码的App.vue还是有很多要学的。

<transition/>

一个动画过渡标签
template

<!--    过渡标签-->
<!--    mode="out-in"表示淡入淡出-->
<transition name="router-fade" mode="out-in">
....
</transition>

css

//  transition标签实现过渡需要的类名
.router-fade-enter-active, .router-fade-leave-active {
    transition: opacity .3s;
}
.router-fade-enter, .router-fade-leave-active {
    opacity: 0;
}

<keep-alive>

keep-alive标签用来缓存不活动的组件实例,避免多次加载相应的组件,减少性能消耗。该标签只能包含一个元素,本项目用它包裹router-view,缓存项目中需要被缓存的页面。在下一小节的vue-router中可知怎么选择页面进行缓存。

<keep-alive>
....
</keep-alive>

vue-router

创建router.js

import {createRouter, createWebHashHistory} from "vue-router";
import LoginView from "../page/login/login.vue";
import index from "../page/index.vue"

// 创建路由对象
const router = createRouter({
    history: createWebHashHistory(), // 采用哈希路由模式
    // 路由规则
    routes:[
        {
            path:'/login', //路由地址
            component:LoginView //切换路由时展示的组件
        },
        {
            path: '/',
            component: index,
        }
    ]
})

export default router

在main.js引入router.js

import { createApp } from 'vue';
import router from "./router/router.js";
import pinia from "./store/store.js";
import App from './App.vue'
import './config/rem'

const app = createApp(App)
app.use(pinia)
app.use(router)
app.mount('#app')

在App.vue中使用

<template>
  <div>
    <!--    过渡标签-->
    <!--    mode="out-in"表示淡入淡出-->
    <transition name="router-fade" mode="out-in">
      <keep-alive>
<!--        设置meta,说明:需要进行缓存的组件进行设置keepAlive: true 不需要缓存的不设置或者设置keepAlive: false-->
        <router-view v-if="$route.meta.keepAlive"></router-view>
      </keep-alive>
    </transition>
<!--    下面这个router-view是用来加载不缓存的组件-->
<!--    svg-icon:调用svg.vue,使其他页面也能使用svg-->
    <transition name="router-fade" mode="out-in">
      <router-view v-if="!$route.meta.keepAlive"></router-view>
    </transition>
    <svg-icon></svg-icon>
  </div>
</template>

keep-alive标签只给有$route.meta.keepAlive属性的router-view进行缓存,而该属性可在router.js中实现(我这还没实现,源码实现了)。


用户代理样式表

用户代理样式表是浏览器(例如,Chrome,Firefox,Edge 等)提供的“默认样式表”,用于以满足“一般显示期望”的方式显示页面。这样式表导致我以为是我样式哪写错了。用户代理样式表要自己写样式进行覆盖,源码用common.scss进行覆盖。
common.scss

body, div, span, header, footer, nav, section, aside, article, ul, dl, dt, dd, li, a, p, h1, h2, h3, h4,h5, h6, i, b, textarea, button, input, select, figure, figcaption, {
    padding: 0;
    margin: 0;
    list-style: none;
    font-style: normal;
    text-decoration: none;
    border: none;
    color: #333;
    font-weight: normal;
    font-family: "Microsoft Yahei";
    box-sizing: border-box;
    -webkit-tap-highlight-color:transparent;
    -webkit-font-smoothing: antialiased;
    &:hover{
        outline: none;
    }
}

/*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/
::-webkit-scrollbar
{
    width: 0px;
    height: 0px;
    background-color: #F5F5F5;
}

/*定义滚动条轨道 内阴影+圆角*/
::-webkit-scrollbar-track
{
    -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0);
    border-radius: 10px;
    background-color: #F5F5F5;
}

/*定义滑块 内阴影+圆角*/
::-webkit-scrollbar-thumb
{
    border-radius: 10px;
    -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
    background-color: #555;
}

input[type="button"], input[type="submit"], input[type="search"], input[type="reset"] {
    -webkit-appearance: none;
}

textarea { -webkit-appearance: none;}

html,body{
    height: 100%;
    width: 100%;
    background-color: #F5F5F5;
}


.clear:after{
    content: '';
    display: block;
    clear: both;
}

.clear{
    zoom:1;
}

.back_img{
    background-repeat: no-repeat;
    background-size: 100% 100%;
}

.margin{
    margin: 0 auto;
}

.left{
    float: left;
}

.right{
    float: right;
}

.hide{
    display: none;
}

.show{
    display: block;
}

.ellipsis{
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.paddingTop{
    padding-top: 1.95rem;
}

@keyframes backOpacity{
    0%   { opacity: 1 }
    25%  { opacity: .5 }
    50%  { opacity: 1 }
    75%  { opacity: .5 }
    100% { opacity: 1 }
}

.animation_opactiy{
    animation: backOpacity 2s ease-in-out infinite;
}

之后导入到App.vue中,记得App.vue的style不要添加scoped,不然common.scss只能应用到App.vue中。

<style lang="scss">
  @import './style/common.scss';

  //  transition标签实现过渡需要的类名
  .router-fade-enter-active, .router-fade-leave-active {
    transition: opacity .3s;
  }
  .router-fade-enter, .router-fade-leave-active {
    opacity: 0;
  }
</style>

login.vue

login.vue其实没什么难点,但登录时会碰到验证码失效的问题,一直登不上。

验证码失效

当时想的是为啥验证码数字传后台,后台就能核对你的数字是否正确,验证码图片我是从后台获取的,但图片在登录时并没有传回去。我查了一下源码作者的饿了吗后端代码,发现应该是验证码图片的数字是通过cookie传的。
后端代码

但我用document.cookie获取输出的cookie是空的,原项目登录是成功的,但也获取不到cookie,这卡了太久就先放那吧。

效果

login.vue

代码

login.vue

<template>
  <div class="loginContainer">
    <head-top :head-title="'密码登录'" go-back="true"></head-top>
    <form class="loginForm">
      <section class="input_container">
        <input type="text" placeholder="账号" v-model.lazy="userAccount">
      </section>
      <section class="input_container">
        <input v-if="!showPassword" type="password" placeholder="密码" v-model="passWord">
        <input v-else type="text" placeholder="密码" v-model="passWord">
        <div class="button_switch" :class="{change_to_text:showPassword}">
          <div class="circle_button" :class="{trans_to_right: showPassword}" @click="changePassWordType"></div>
          <span>abc</span>
          <span>...</span>
        </div>
      </section>
      <section class="input_container captcha_code_container">
        <input type="text" placeholder="验证码" maxlength="4" v-model="codeNumber">
        <div class="img_change_img">
          <img v-show="captchaCodeImg" :src="captchaCodeImg">
          <div class="change_img" @click="getCaptchaCode">
            <p>看不清</p>
            <p>换一张</p>
          </div>
        </div>
      </section>
    </form>
    <p class="login_tips">
      温馨提示:未注册过的账号,登录时将自动注册
    </p>
    <p class="login_tips">
      注册过的用户可凭账号密码登录
    </p>
    <div class="login_container" @click="login">登录</div>
    <router-link to="/forget" class="to_forget">重置密码?</router-link>
    <alert-tip v-if="showAlert" :showHide="showAlert" @closeTip="closeTip" :alertText="alertText"></alert-tip>
  </div>
</template>

<script>
  import headTop from "../../components/header/head.vue";
  import AlertTip from "../../components/common/alertTip.vue";
  import pinia from "../../store/store.js";
  import {useMainStore} from "../../store/index.js";
  import {accountLogin, getCaptchas} from "../../service/getData.js";
  export default {
    name:"login",
    data:()=>({
      mainStore: null,
      showPassword: false, // 是否显示密码
      phoneNumber: null, //电话号码
      computedTime: 0, //倒数记时
      userInfo: null, //获取到的用户信息
      userAccount: null, //用户名
      passWord: null, //密码
      captchaCodeImg: null, //验证码地址
      codeNumber: null, //验证码
      showAlert: false, //显示提示组件
      alertText: null, //提示的内容
    }),
    created() {
      this.getCaptchaCode();
      this.mainStore = useMainStore(pinia);
    },
    components: {
      AlertTip,
      headTop,
    },
    computed: {
      rightPhoneNumber: function (){
        // 正则表达式判断是否是手机号
        // https://zhidao.baidu.com/question/2080522189597073148.html
        return /^1\d{10}$/gi.test(this.phoneNumber)
      }
    },
    methods:{
      getCookieObj() {//根据name获取cookie的值
        var aCookie = document.cookie.split(";");
        var re = '';
        for (var i = 0; i < aCookie.length; i++) {
          var aCrumb = aCookie[i].split("=");
          if(aCrumb[0].toString().trim()=='order_list'){
            continue;
          }
          re += (aCrumb[0] + " = " + aCrumb[1] + '\n\n');
        }
        return re;
      },
      // 是否显示密码
      changePassWordType(){
        this.showPassword = !this.showPassword;
      },
      // 获取验证码,线上环境使用固定的图片,生产环境使用真实的验证码
      async getCaptchaCode(){
        let res = await getCaptchas();
        this.captchaCodeImg = res.code;
      },
      async login(){
        if (!this.userAccount){
          this.showAlert = true;
          this.alertText = '请输入手机号/邮箱/用户名';
          return
        }else if (!this.passWord){
          this.showAlert = true;
          this.alertText = '请输入密码';
          return
        }else if (!this.codeNumber){
          this.showAlert = true;
          this.alertText = '请输入验证码';
          return
        }
        console.log(this.codeNumber)
        let c = this.getCookieObj();
        console.log("cookie:")
        console.log(document.cookie)
        this.userInfo = await accountLogin(this.userAccount, this.passWord, this.codeNumber);
        console.log(this.userInfo)
        // 如果登录获取的返回值不正确,则弹出提示框,返回值正确则返回上一页
        if (!this.userInfo.user_id){
          this.showAlert = true;
          this.alertText = this.userInfo.message;
          this.getCaptchaCode();
        }else {
          this.mainStore.recordUserInfo(this.userInfo);
          this.$router.go(-1);
        }
      },
      closeTip(){
        this.showAlert = false;
      }
    }
  }
</script>

<style lang="scss" scoped>
  @import "../../style/mixin.scss";

  // 去除密码框自带的小眼睛
  input[type="password"]::-ms-reveal,::-ms-clear{
    display: none;
  }

  .loginContainer{
    padding-top: 1.95rem; //把页面主体向下顶,避免被head.vue覆盖
    p,span,input{
      font-family: Helvetica Neue,Tahoma,Arial;
    }
  }

  .loginForm{
    background-color: white;
    margin-top: .6rem;
    .input_container{
      display: flex;
      justify-content: space-between;
      padding: .6rem .8rem;
      border-bottom: 1px solid #f1f1f1;
      // input有默认长度,不会因flex占满空间
      // input有默认高度,所以.login_container会自动有个高度
      input{
        @include sc(.7rem, #666);
      }
    }
    .captcha_code_container{
      height: 2.2rem;
      .img_change_img{
        display: flex;
        align-items: center;
        img{
          @include wh(3.5rem,1.5rem);
          margin-right: .2rem;
        }
        .change_img{
          display: flex;
          flex-direction: column;
          flex-wrap: wrap;
          justify-content: center;
          p{
            @include sc(.55rem, #666);
          }
          p:nth-of-type(2){
            color: #3b95e9;
            margin-top: .2rem;
          }
        }
      }
    }

    .button_switch{
      background-color: #ccc;
      display: flex;
      justify-content: center;
      @include wh(2rem,.7rem);
      padding: 0 .2rem;
      border: 1px;
      border-radius: .5rem;
      position: relative;
      .circle_button{
        transition: all .3s;
        position: absolute;
        top: -0.2rem;
        z-index: 1;
        left: -0.3rem;
        @include wh(1.2rem,1.2rem);
        box-shadow: 0 0.026667rem 0.053333rem 0 rgba(0,0,0,.1);
        background-color: #f1f1f1;
        border-radius: 50%;
      }
      .trans_to_right{
        transform: translateX(1.3rem);
      }
      span{
        @include sc(.45rem, #fff);
        transform: translateY(.05rem);
        line-height: .6rem;
      }
      span:nth-of-type(2){
        transform: translateY(-.06rem);
      }
    }
    .change_to_text{
      background-color: #4cd964;
    }
  }

  .login_tips{
    @include sc(.5rem, red);
    padding: .4rem .6rem;
    line-height: .5rem;
    a{
      color: #3b95e9;
    }
  }

  .login_container{
    margin: 0 .5rem 1rem;
    @include sc(.7rem, #fff);
    background-color: #4cd964;
    padding: .5rem 0;
    border: 1px;
    border-radius: 0.15rem;
    text-align: center;
  }

  .to_forget{
    float: right;
    @include sc(.6rem, #3b95e9);
    margin-right: .3rem;
  }
</style>

router.js

import {createRouter, createWebHashHistory} from "vue-router";
import LoginView from "../page/login/login.vue";
import index from "../page/index.vue"

// 创建路由对象
const router = createRouter({
    history: createWebHashHistory(), // 采用哈希路由模式
    // 路由规则
    routes:[
        {
            path:'/login', //路由地址
            component:LoginView //切换路由时展示的组件
        },
        {
            path: '/',
            component: index,
        }
    ]
})

export default router

main.js

import { createApp } from 'vue';
import router from "./router/router.js";
import pinia from "./store/store.js";
import App from './App.vue'
import './config/rem'

const app = createApp(App)
app.use(pinia)
app.use(router)
app.mount('#app')

App.vue

<script>
import svgIcon from './components/common/svg.vue';
export default {
  components: {
    svgIcon,
  },
  methods:{
  }
}
</script>

<template>
  <div>
    <!--    过渡标签-->
    <!--    mode="out-in"表示淡入淡出-->
    <transition name="router-fade" mode="out-in">
      <keep-alive>
        <router-view v-if="$route.meta.keepAlive"></router-view>
      </keep-alive>
    </transition>
    <transition name="router-fade" mode="out-in">
      <router-view v-if="!$route.meta.keepAlive"></router-view>
    </transition>
    <svg-icon></svg-icon>
  </div>

</template>

<style lang="scss">
  @import './style/common.scss';

  //  transition标签实现过渡需要的类名
  .router-fade-enter-active, .router-fade-leave-active {
    transition: opacity .3s;
  }
  .router-fade-enter, .router-fade-leave-active {
    opacity: 0;
  }

</style>

pinia的index.js

import {defineStore} from "pinia";
import {getUser} from "../service/getData.js";
import {setStore} from "../config/mUtils.js";

// pinia:https://www.bilibili.com/video/BV11Y411b7nb?p=2&vd_source=e0c7172c94c024018a8732279ef8d1fe
// 定义并导出容器
export const useMainStore = defineStore('main',{
    // 类似于组件的data,用来存储全局状态
    // 必须是函数,且是箭头函数
    state(){
        return {
            userInfo:null,
            login:false,
        };
    },

    // 类似于组件的computed,用来封装计算属性,有缓存的功能
    getters:{},

    // 类似于组件的methods,用来封装业务逻辑,修改state
    actions:{
        async getUserInfo(){
            this.userInfo = await getUser();
            this.userInfo = null
        },
        recordUserInfo(info){
            this.userInfo = info;
            this.login = true;
            setStore('user_id',info.user_id);
        }
    }
})

// 使用容器中的state

// 修改state

// action的使用

getData.js

import fetch from "../config/fetch.js";
import {getStore} from "../config/mUtils.js";
/**
 * 获取用户信息
 */
export const getUser = () => fetch('/v1/user', {user_id: "77785"});

// 获取验证码
export const getCaptchas = () => fetch('/v1/captchas', {},'POST');

// 账号密码登录
export const accountLogin = (username,password,captcha_code) =>
    fetch('/v2/login', {username,password,captcha_code},'POST');
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容