一、说明
1.1 界面相关效果
- 切换登陆方式
- 手机号合法检查
- 倒计时效果
- 切换显示或隐藏密码
- 前台验证提示
1.2 前后台交互功能
- 动态一次性图形验证码
- 动态一次性短信验证码
- 短信登陆
- 密码登陆
- 获取用户信息,实现自动登陆
- 退出登陆
二、vuex
2.1 state.js
userInfo: {}, // 用户信息
2.2 mutation-types.js
export const RECEIVE_USER_INFO = 'receive_user_info' // 接收用户信息 export const RESET_USER_INFO = 'reset_user_info' // 重置用户信息
2.3 mutations.js
[RESET_USER_INFO](state) { state.userInfo = {} }, [RECEIVE_USER_INFO](state, {userInfo}) { state.userInfo = userInfo }
2.4 actions.js
// 记录用户信息 recordUserInfo({commit}, userInfo) { commit(RECEIVE_USER_INFO, {userInfo: userInfo}) }, // 异步获取用户信息 async getUserInfo({commit}) { const result = await reqUser() if (result.code === 0) { commit(RECEIVE_USER_INFO, {UserInfo: result.data}) } }, // 退出登陆 async logout({commit}) { const result = await reqLogout() if (result.code === 0) { commit(RESET_USER_INFO) } }
三、Msite.vue
<template> <section class="msite"> <!--首页头部--> <HeaderTop :title="address.name"> <router-link slot="search" to="/search" class="header_search"> <i class="iconfont icon-sousuo"></i> </router-link> <router-link slot="login" :to="userInfo._id ? '/userinfo' : '/login'" class="header_login"> <span class="header_login_text" v-if="!userInfo._id">登录|注册</span> <span class="header_login_text" v-else> <i class="iconfont icon-person"></i> </span> </router-link> </HeaderTop> <!--首页导航--> <nav class="msite_nav"> <div class="swiper-container" v-if="categorysArr.length > 0"> <div class="swiper-wrapper"> <div class="swiper-slide" v-for="(cs, index) in categorysArr" :key="index"> <a href="javascript:" class="link_to_food" v-for="(c, index2) in cs" :key="index2"> <div class="food_container"> <img :src="imgBaseUrl + c.image_url"> </div> <span>{{c.title}}</span> </a> </div> </div> <!-- Add Pagination --> <div class="swiper-pagination"></div> </div> <img src="./images/msite_back.svg" v-else> </nav> <!--首页附近商家--> <div class="msite_shop_list"> <div class="shop_header"> <i class="iconfont icon-xuanxiang"></i> <span class="shop_header_title">附近商家</span> </div> <ShopList/> </div> </section> </template> <script> import Swiper from 'swiper' import 'swiper/dist/css/swiper.min.css' import HeaderTop from '../../components/HeaderTop/HeaderTop' import ShopList from '../../components/ShopList/ShopList' import {mapState} from 'vuex' export default { name: 'Msite', data () { return { imgBaseUrl: 'https://fuss10.elemecdn.com' } }, mounted() { this.$store.dispatch('getShops') this.$store.dispatch('getCategorys') }, computed: { ...mapState(['address', 'categorys', 'userInfo']), categorysArr () { const max = 8 const arr = [] const {categorys} = this let smallArr = [] categorys.forEach((c, index) => { if(smallArr.length === 0) { arr.push(smallArr) } smallArr.push(c) if(smallArr.length === max) { smallArr = [] } }) return arr } }, components: { HeaderTop, ShopList }, watch: { categorys(value) { this.$nextTick(() => { new Swiper('.swiper-container', { pagination: { el: '.swiper-pagination' }, loop: true }) }) } } } </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixins.styl" &.msite //首页 width 100% .msite_nav bottom-border-1px(#e4e4e4) margin-top 45px height 200px background #fff .swiper-container width 100% height 100% .swiper-wrapper width 100% height 100% .swiper-slide display flex justify-content center align-items flex-start flex-wrap wrap .link_to_food width 25% .food_container display block width 100% text-align center padding-bottom 10px font-size 0 img display inline-block width 50px height 50px span display block width 100% text-align center font-size 13px color #666 .swiper-pagination >span.swiper-pagination-bullet-active background #02a774 .msite_shop_list top-border-1px(#e4e4e4) margin-top 10px background #fff .shop_header padding 10px 10px 0 .shop_icon margin-left 5px color #999 .shop_header_title color #999 font-size 14px line-height 20px </style>
四、Profile.vue
<template> <section class="profile"> <HeaderTop title="我的"></HeaderTop> <section class="profile-number"> <router-link :to="userInfo._id ? '/userinfo' : '/login'" class="profile-link"> <div class="profile_image"> <i class="iconfont icon-person"></i> </div> <div class="user-info"> <p class="user-info-top" v-if="!userInfo.phone">{{userInfo.name || '登录/注册'}}</p> <p> <span class="user-icon"> <i class="iconfont icon-shouji icon-mobile"></i> </span> <span class="icon-mobile-number"> {{userInfo.phone || '暂无绑定手机号'}}</span> </p> </div> <span class="arrow"> <i class="iconfont icon-jiantou1"></i> </span> </router-link> </section> <section class="profile_info_data border-1px"> <ul class="info_data_list"> <a href="javascript:" class="info_data_link"> <span class="info_data_top"><span>0.00</span>元</span> <span class="info_data_bottom">我的余额</span> </a> <a href="javascript:" class="info_data_link"> <span class="info_data_top"><span>0</span>个</span> <span class="info_data_bottom">我的优惠</span> </a> <a href="javascript:" class="info_data_link"> <span class="info_data_top"><span>0</span>分</span> <span class="info_data_bottom">我的积分</span> </a> </ul> </section> <section class="profile_my_order border-1px"> <!-- 我的订单 --> <a href='javascript:' class="my_order"> <span> <i class="iconfont icon-order-s"></i> </span> <div class="my_order_div"> <span>我的订单</span> <span class="my_order_icon"> <i class="iconfont icon-jiantou1"></i> </span> </div> </a> <!-- 积分商城 --> <a href='javascript:' class="my_order"> <span> <i class="iconfont icon-jifen"></i> </span> <div class="my_order_div"> <span>积分商城</span> <span class="my_order_icon"> <i class="iconfont icon-jiantou1"></i> </span> </div> </a> <!-- 硅谷外卖会员卡 --> <a href="javascript:" class="my_order"> <span> <i class="iconfont icon-vip"></i> </span> <div class="my_order_div"> <span>硅谷外卖会员卡</span> <span class="my_order_icon"> <i class="iconfont icon-jiantou1"></i> </span> </div> </a> </section> <section class="profile_my_order border-1px"> <!-- 服务中心 --> <a href="javascript:" class="my_order"> <span> <i class="iconfont icon-fuwu"></i> </span> <div class="my_order_div"> <span>服务中心</span> <span class="my_order_icon"> <i class="iconfont icon-jiantou1"></i> </span> </div> </a> </section> <section class="profile_my_order border-1px" v-if="userInfo._id"> <mt-button type="danger" style="width: 100%" @click="logout">退出登录</mt-button> </section> </section> </template> <script> import HeaderTop from '../../components/HeaderTop/HeaderTop' import {mapState} from 'vuex' import { MessageBox} from 'mint-ui'; export default { name: 'Profile', components: { HeaderTop }, computed: { ...mapState(['userInfo']) }, methods: { logout () { MessageBox.confirm('确定退出登陆吗?').then(action => { this.$store.dispatch('logout') }) } } } </script> <style lang="stylus" rel="stylesheet/stylus"> @import "../../common/stylus/mixins.styl" &.profile //我的 width 100% .profile-number margin-top 45.5px .profile-link clearFix() position relative display block background #02a774 padding 20px 10px .profile_image float left width 60px height 60px border-radius 50% overflow hidden vertical-align top .icon-person background #e4e4e4 font-size 62px .user-info float left margin-top 8px margin-left 15px p font-weight: 700 font-size 18px color #fff &.user-info-top padding-bottom 8px .user-icon display inline-block margin-left -15px margin-right 5px width 20px height 20px .icon-mobile font-size 30px vertical-align text-top .icon-mobile-number font-size 14px color #fff .arrow width 12px height 12px position absolute right 15px top 40% .icon-jiantou1 color #fff font-size 5px .profile_info_data bottom-border-1px(#e4e4e4) width 100% background #fff overflow hidden .info_data_list clearFix() .info_data_link float left width 33% text-align center border-right 1px solid #f1f1f1 .info_data_top display block width 100% font-size 14px color #333 padding 15px 5px 10px span display inline-block font-size 30px color #f90 font-weight 700 line-height 30px .info_data_bottom display inline-block font-size 14px color #666 font-weight 400 padding-bottom 10px .info_data_link:nth-of-type(2) .info_data_top span color #ff5f3e .info_data_link:nth-of-type(3) border 0 .info_data_top span color #6ac20b .profile_my_order top-border-1px(#e4e4e4) margin-top 10px background #fff .my_order display flex align-items center padding-left 15px >span display flex align-items center width 20px height 20px >.iconfont margin-left -10px font-size 30px .icon-order-s color #02a774 .icon-jifen color #ff5f3e .icon-vip color #f90 .icon-fuwu color #02a774 .my_order_div width 100% border-bottom 1px solid #f1f1f1 padding 18px 10px 18px 0 font-size 16px color #333 display flex justify-content space-between span display block .my_order_icon width 10px height 10px .icon-jiantou1 color #bbb font-size 10px </style>
五、提示框组件: components/AlertTip/AlertTip.vue
<template> <div class="alert_container"> <section class="tip_text_container"> <div class="tip_icon"> <span></span> <span></span> </div> <p class="tip_text">{{alertText}}</p> <div class="confrim" @click="closeTip">确认</div> </section> </div> </template> <script> export default { props: { alertText: String }, methods: { closeTip() { this.$emit('closeTip') } } } </script> <style lang="stylus" rel="stylesheet/stylus"> @import '../../common/stylus/mixins.styl'; @keyframes tipMove 0% transform: scale(1) 35% transform: scale(.8) 70% transform: scale(1.1) 100% transform: scale(1) .alert_container position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 200; background: rgba(0, 0, 0, .5) .tip_text_container position: absolute; top: 50%; left: 50%; margin-top: -90px margin-left: -110px width: 60% animation: tipMove .4s; background-color: rgba(255, 255, 255, 1); border: 1px; padding-top: 20px display: flex; justify-content: center; align-items: center; flex-direction: column; border-radius: 5px .tip_icon width: 55px height: 55px border: 2px solid #f8cb86; border-radius: 50%; font-size 20px display: flex; justify-content: center; align-items: center; flex-direction: column; span:nth-of-type(1) width: 2px height: 30px background-color: #f8cb86; span:nth-of-type(2) width: 2px height: 2px border: 1px; border-radius: 50%; margin-top: 2px background-color #f8cb86 .tip_text font-size 14px color #333 line-height 20px text-align center margin-top 10px padding 0 5px .confrim font-size 18px font-weight bold margin-top 10px background-color #4cd964 width 100% text-align center line-height 35px border 1px color #fff border-bottom-left-radius 5px border-bottom-right-radius 5px </style>
六、使用mint-ui
6.1 主页
http://mint-ui.github.io/#!/zh-cn
6.2 下载
npm install --save mint-ui
6.3 实现按需打包
- 下载:npm install --save-dev babel-plugin-component
- 修改
plugins:[ [ "component", { "libraryName": "mint-ui", "style": true } ] ]
6.4 mint-ui 组件分类
- 标签组件
- 非标签组件
七、使用 mint-ui 的组件
7.1 mian.js
import {
Button,
} from 'mint-ui'
// 注册全局组件
Vue.component(Button.name, Button)
7.2 Profile.vue
<section class="profile_my_order border-1px" v-if="userInfo._id"> <mt-button type="danger" style="width: 100%" @click="logout">退出登录</mt-button> </section> <script> import { MessageBox} from 'mint-ui'; export default { name: 'Profile', computed: { ...mapState(['userInfo']) }, methods: { logout () { MessageBox.confirm('确定退出登陆吗?').then(action => { this.$store.dispatch('logout') }) } } } </script>
八、Login.vue
<template> <section class="loginContainer"> <div class="loginInner"> <div class="login_header"> <h2 class="login_logo">硅谷外卖</h2> <div class="login_header_title"> <a href="javascript:;" :class="{on: !loginWay}" @click="loginWay=false">短信登录</a> <a href="javascript:;" :class="{on: loginWay}" @click="loginWay=true">密码登录</a> </div> </div> <div class="login_content"> <form @submit.prevent="login"> <div :class="{on: !loginWay}"> <section class="login_message"> <input type="tel" maxlength="11" placeholder="手机号" v-model="phone"> <button :disabled="!rightPhone" class="get_verification" :class="{rightPhone: rightPhone}" @click.preven="getCode"> {{computeTime > 0 ? computeTime + 'S' : "获取验证码"}} </button> </section> <section class="login_verification"> <input type="tel" maxlength="8" placeholder="验证码"> </section> <section class="login_hint"> 温馨提示:未注册硅谷外卖帐号的手机号,登录时将自动注册,且代表已同意 <a href="javascript:;">《用户服务协议》</a> </section> </div> <div :class="{on: loginWay}"> <section> <section class="login_message"> <input type="tel" maxlength="11" placeholder="手机/邮箱/用户名" v-model="name"> </section> <section class="login_verification"> <input type="text" maxlength="8" placeholder="密码" v-model="pwd" v-if="showPassword"> <input type="password" maxlength="8" placeholder="密码" v-model="pwd" v-if="!showPassword"> <div class="switch_button" :class="showPassword ? 'on' : 'off'" @click="showPassword=!showPassword"> <div class="switch_circle" :class="{right: showPassword}" :style="{transform: showPassword ? 'translateX(27px)' : 'translateX(0px)'}"></div> <span class="switch_text">{{showPassword ? 'abc' : '...'}}</span> </div> </section> <section class="login_message"> <input type="text" maxlength="11" placeholder="验证码" v-model="captcha"> <img class="get_verification" src="http://localhost:4000/captcha" @click="getCaptcha" ref="captcha"> </section> </section> </div> <button class="login_submit">登录</button> </form> <a href="javascript:;" class="about_us">关于我们</a> </div> <a href="javascript:" @click="$router.back()" class="go_back"> <i class="iconfont icon-jiantou2"></i> </a> </div> <AlertTip :alert-text="alertText" v-show="showAlert" @closeTip="closeTip"></AlertTip> </section> </template> <script> import {reqPwdLogin, reqSendCode, reqSmsLogin} from "../../api" import AlertTip from '../../components/AlertTip/AlertTip' export default { name: 'Login', data() { return { loginWay: true, // true 代表密码登陆, false 代表短信登陆 computeTime: 0, showPassword: false, // 是否显示密码 phone: '', // 手机号 code: '', // 短信验证码 name: '', // 用户名 pwd: '', // 密码 captcha: '', // 图形验证码 showAlert: false, // 是否显示提示框 alertText: '', // 提示框文本 } }, mounted() { this.name = '' this.pwd = '' }, computed: { rightPhone() { return /^1\d{10}$/.test(this.phone) } }, components: { AlertTip }, methods: { // 获取短信验证码 async getCode() { if (this.computeTime === 0) { this.computeTime = 60 this.intervalId = setInterval(() => { this.computeTime-- if (this.computeTime === 0) { clearInterval(this.intervalId) } }, 1000) // 发送短信验证码 let result = await reqSendCode(this.phone) if (result.code === 1) { // 显示提示框 this.showAlert = true this.alertText = result.msg // 停止倒计时 if (this.computeTime) { this.computeTime = 0 clearInterval(this.intervalId) } } } }, // 获取图形验证码 getCaptcha() { this.$refs.captcha.src = 'http://localhost:4000/captcha?time=' + Date.now() }, // 关闭提示框 closeTip() { this.showAlert = false this.alertText = '' }, // 发送登录信息 async login() { if (!this.loginWay) { if (!this.rightPhone) { this.showAlert = true this.alertText = '手机号码不正确' return } else if (!(/^\d{6}&/gi.test(this.code))) { this.showAlert = true this.alertText = '短信验证码不正确' return } // 手机号短信登录 const result = await reqSmsLogin(this.phone, this.code) if (result.code === 0) { this.userInfo = result.data } else { this.userInfo = { msg: '登录失败, 手机号或验证码不正确' } } } else { if (!this.name) { this.showAlert = true this.alertText = '请输入手机号/邮箱/用户名' return } else if (!this.pwd) { this.showAlert = true this.alertText = '请输入密码' return } else if (!this.captcha) { this.showAlert = true this.alertText = '请输入验证码' return } // 用户名登录 const result = await reqPwdLogin(this.name, this.pwd, this.captcha) if (result.code === 0) { this.userInfo = result.data this.$store.dispatch('recordUserInfo', this.userInfo) this.$router.push('/profile') } else { this.showAlert = true this.alertText = result.msg this.getCaptcha() this.captcha = '' } } }, } } </script> <style lang="stylus" rel="stylesheet/stylus"> .loginContainer width 100% height 100% background #fff .loginInner padding-top 60px width 80% margin 0 auto .login_header .login_logo font-size 40px font-weight bold color #02a774 text-align center .login_header_title padding-top 40px text-align center >a color #333 font-size 14px padding-bottom 4px &:first-child margin-right 40px &.on color #02a774 font-weight 700 border-bottom 2px solid #02a774 .login_content >form >div display none &.on display block input width 100% height 100% padding-left 10px box-sizing border-box border 1px solid #ddd border-radius 4px outline 0 font 400 14px Arial &:focus border 1px solid #02a774 .login_message position relative margin-top 16px height 48px font-size 14px background #fff .get_verification position absolute top 50% right 10px transform translateY(-50%) border 0 color #ccc font-size 14px background transparent &.rightPhone color black .login_verification position relative margin-top 16px height 48px font-size 14px background #fff .switch_button font-size 12px border 1px solid #ddd border-radius 8px transition background-color .3s,border-color .3s padding 0 6px width 30px height 16px line-height 16px color #fff position absolute top 50% right 10px transform translateY(-50%) &.off background #fff .switch_text float right color #ddd &.on background #02a774 >.switch_circle //transform translateX(27px) position absolute top -1px left -1px width 16px height 16px border 1px solid #ddd border-radius 50% background #fff box-shadow 0 2px 4px 0 rgba(0,0,0,.1) transition transform .3s .login_hint margin-top 12px color #999 font-size 14px line-height 20px >a color #02a774 .login_submit display block width 100% height 42px margin-top 30px border-radius 4px background #4cd96f color #fff text-align center font-size 16px line-height 42px border 0 .about_us display block font-size 12px margin-top 20px text-align center color #999 .go_back position absolute top 5px left 5px width 30px height 30px >.iconfont font-size 20px color #999 </style>