cxmo 1 жил өмнө
parent
commit
394956f0a9

+ 79 - 1
src/api/user.js

@@ -1,7 +1,7 @@
 import {get,post} from './index'
 
 /**
- * 登录
+ * 登录 (ETA1.4后弃用)
  * @param Username
  * @param Password
  * @param IsRemember
@@ -16,3 +16,81 @@ export function apiLogin(params){
 export function apiMenuList(params){
     return get('/system/menu/list',params)
 }
+
+export const _apiLogin = {
+    /**
+     * 获取图形验证码,返回base64格式图片
+     * @returns 
+     */
+    apiGetPicCode:(params)=>{
+        return get('/user_login/get_captcha',params)
+    },
+    /**
+     * 获取短信/邮箱验证码
+     * @param {Number} VerifyType 验证方式: 1-手机号; 2-邮箱
+     * @param {String} CaptchaId 图形验证码ID
+     * @param {String} CaptchaCode 图形验证码的值
+     * @param {String} Mobile 手机号 验证方式为1时必填
+     * @param {String} Email 邮箱 验证方式为2时必填
+     * @param {String} TelAreaCode 手机区号
+     * @param {Number} Source 来源:1-登录;3-忘记密码
+     * @returns 
+     */
+    apiGetVerifyCode:(params)=>{
+        return post('/user_login/verify_code',params)
+    },
+    /**
+     * ETA1.4后,用户登录
+     * @param {Number} LoginType 登录方式: 1-账号; 2-手机号; 3-邮箱
+     * @param {String} Username 用户名
+     * @param {String} Password 密码
+     * @param {String} Mobile 手机号
+     * @param {String} Email 邮箱
+     * @param {String} VerifyCode 手机号/邮箱的验证码
+     * @returns 
+     */
+    apiUserLogin:(params)=>{
+        return post('/user_login/login',params)
+    },
+    /**
+     * 忘记密码账号校验,获取账号信息
+     * @param {String} CaptchaId 图形验证码ID
+     * @param {String} CaptchaCode 图形验证码
+     * @param {String} UserName 账号
+     * @returns 
+    */
+    apiAccountCheck:(params)=>{
+        return post('/user_login/forget/account_get',params)
+    },
+    /**
+     * 忘记密码-手机/邮箱的验证码校验
+     * @param {Number} FindType 密码找回方式:1-手机号;2-邮箱
+     * @param {String} VerifyCode 验证码
+     * @param {String} UserName 用户名
+     * @param {String} Mobile 找回方式为手机号时必填
+     * @param {String} Email 找回方式为邮箱时必填
+     * @returns 
+     */
+    apiCheckCodeVerify:(params)=>{
+        return post('/user_login/forget/code_verify',params)
+    },
+    /**
+     * 忘记密码-重置密码
+     * @param {String} UserName 
+     * @param {String} Password 
+     * @param {String} RePassword 
+     * @returns 
+     */
+    resetPass:(params)=>{
+        return post('/user_login/forget/reset_pass',params)
+    },
+    /**
+     * 获取手机号区号
+     * @returns 
+     */
+    getPhoneAreaCode:(params)=>{
+        return get('/user_login/area_code/list',params)
+    },
+}
+
+

BIN
src/assets/imgs/login/account-icon.png


BIN
src/assets/imgs/login/email-icon.png


BIN
src/assets/imgs/login/phone-icon.png


+ 169 - 41
src/views/Login.vue

@@ -1,16 +1,26 @@
 <script setup>
 import {ref} from 'vue'
 import { showToast } from 'vant';
-import {apiLogin} from '@/api/user'
+import {apiLogin,_apiLogin} from '@/api/user'
 import md5 from 'js-md5'
 import {Base64} from 'js-base64'
 import { useRouter } from 'vue-router';
+import MobileModel from './login/MobileModel.vue';
+import OrdinaryModel from './login/OrdinaryModel.vue';
+import EmailModel from './login/EmailModel.vue';
+import ForgetPassModel from './login/ForgetPassModel.vue';
 
 const router=useRouter()
 
-
-let username=ref(localStorage.getItem('account')?Base64.decode(localStorage.getItem('account')):'')
-let password=ref(localStorage.getItem('checkPass')?Base64.decode(localStorage.getItem('checkPass')):'')
+//登陆模式 ordinaryModel账号密码 mobileModel手机号 emailModel邮箱
+let activeModel = ref('ordinaryModel')
+let autoAccount = ref('')
+function changeModel(model){
+    if(model==='forgetPassModel'){
+        autoAccount.value = loginForm.value.getValues().username
+    }
+    activeModel.value = model
+}
 
 const onSubmit = (values) => {
     const params={
@@ -22,7 +32,7 @@ const onSubmit = (values) => {
         if(res.Ret===200){
             localStorage.setItem('token',res.Data.Authorization)
             localStorage.setItem('account',Base64.encode(values.username))
-			localStorage.setItem('checkPass',Base64.encode(values.password))
+            localStorage.setItem('checkPass',Base64.encode(values.password))
             const userInfo={
                 AdminName:res.Data.AdminName,
                 Authority:res.Data.Authority,
@@ -39,40 +49,143 @@ const onSubmit = (values) => {
         }
     })
 };
+
+
+let loginForm = ref(null)
+let mobileModel = ref(null)
+let emailModel = ref(null)
+const onSubmit2 = ()=>{
+    //根据activeModel验证值
+    const values = loginForm.value.getValues()
+    if(activeModel.value==='ordinaryModel'){
+        loginForm.value.validate(['username','password']).then(()=>{
+            userLogin({
+                LoginType:1,
+                Username:values.username,
+                Password:md5(values.password),
+            },values)
+        }).catch((error)=>{
+            console.log('error',error)
+        })
+    }
+
+    if(activeModel.value==='mobileModel'){
+        loginForm.value.validate(['mobile','mobileCode']).then(()=>{
+            userLogin({
+                LoginType:2,
+                Mobile:values.mobile,
+                VerifyCode:values.mobileCode
+            })
+        }).catch((error)=>{
+            console.log('error',error)
+        })
+    }
+    
+    if(activeModel.value==='emailModel'){
+        loginForm.value.validate(['email','emailCode']).then(()=>{
+            userLogin({
+                LoginType:3,
+                Email:values.email,
+                VerifyCode:values.emailCode
+            })
+        }).catch((error)=>{
+            console.log('error',error)
+        })
+    }
+}
+function userLogin(params,values){
+    _apiLogin.apiUserLogin(params).then(res=>{
+        //账号异常,提醒用户用其他方式登陆
+        if(res.Ret===4011){
+            showToast('您的账号异常,请使用其他方式登陆')
+        }
+        //账号或密码错误,弹窗提示就行,因为PC端有特殊提示要求所以单独设置了状态码
+        if(res.Ret===4012){
+            showToast('账号或密码错误,请检查')
+            return
+        }
+        if(res.Ret!==200){
+            activeModel.value!=='ordinaryModel'&&[activeModel].value.getCodePic()
+        }
+
+        localStorage.setItem('token',res.Data.Authorization)
+        activeModel.value==='ordinaryModel'&&localStorage.setItem('account',Base64.encode(values.username))
+        activeModel.value==='ordinaryModel'&&localStorage.setItem('checkPass',Base64.encode(values.password))
+        const userInfo={
+            AdminName:res.Data.AdminName,
+            Authority:res.Data.Authority,
+            ProductName:res.Data.ProductName,
+            RealName:res.Data.RealName,
+            RoleName:res.Data.RoleName,
+            RoleTypeCode:res.Data.RoleTypeCode,
+            SysRoleTypeCode:res.Data.SysRoleTypeCode,
+            AdminId:res.Data.AdminId,
+            DepartmentName:res.Data.DepartmentName
+        }
+        localStorage.setItem('userInfo',Base64.encode(JSON.stringify(userInfo)))
+        router.replace('/')
+    })
+}
 </script>
 
 <template>
     <div class="login-page">
-        <van-form class="form-box" @submit="onSubmit">
+        <van-form class="form-box" ref="loginForm" v-if="activeModel!=='forgetPassModel'">
             <div class="logo-wrap">
                 <img class="logo" src="@/assets/imgs/logo_icon.png" alt="">
                 <div class="title">Bind on account,</div>
                 <div class="sub-title">sign in to continue</div>
             </div>
-            <van-field
-                class="form-input-box"
-                v-model="username"
-                name="username"
-                placeholder="请输入管理后台账号"
-                :rules="[{ required: true, message: '请填写账号' }]"
-            />
-            <van-field
-                class="form-input-box"
-                v-model="password"
-                type="password"
-                name="password"
-                placeholder="请输入密码"
-                :rules="[{ required: true, message: '请填写密码' }]"
-            />
+            <div class="ordinaryModel" v-show="activeModel==='ordinaryModel'">
+                <ordinary-model :activeModel="activeModel" @change-model="changeModel('forgetPassModel')"/>
+            </div>
+            <div class="mobileModel" v-show="activeModel==='mobileModel'">
+                <mobile-model ref="mobileModel"
+                    :activeModel="activeModel"/>
+            </div>
+            <div class="emailModel" v-show="activeModel==='emailModel'">
+                <email-model ref="emailModel"
+                    :activeModel="activeModel"/>
+            </div>
             <div class="btn-box">
                 <van-button 
                     round 
                     block 
                     type="primary" 
-                    native-type="submit"
+                    @click="onSubmit2"
                 >登录</van-button>
             </div>
+            <!-- 选择其他登陆方法 -->
+            <div class="model-icon-box">
+                <div class="model-item" 
+                    @click="changeModel('ordinaryModel')"
+                    v-show="activeModel!=='ordinaryModel'">
+                    <div class="model-item-icon">
+                        <img src="@/assets/imgs/login/account-icon.png"/>
+                    </div>
+                </div>
+                <div class="model-item" 
+                    @click="changeModel('mobileModel')"
+                    v-show="activeModel!=='mobileModel'">
+                    <div class="model-item-icon">
+                        <img src="@/assets/imgs/login/phone-icon.png"/>
+                    </div>
+                    
+                </div>
+                <div class="model-item" 
+                    @click="changeModel('emailModel')"
+                    v-show="activeModel!=='emailModel'">
+                    <div class="model-item-icon">
+                        <img src="@/assets/imgs/login/email-icon.png"/>
+                    </div>
+                </div>
+            </div>
         </van-form>
+        <div class="form-box" v-else>
+            <forget-pass-model ref="forgetPassModel"
+                :autoAccount="autoAccount"
+                @change-model="changeModel('ordinaryModel')"/>
+        </div>
         <img class="pad-img" src="https://hzstatic.hzinsights.com/static/ETA_mobile/login_img.png" alt="">
         <div class="mobile-bot-text">Long time&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;More profit</div>
     </div>
@@ -100,16 +213,24 @@ const onSubmit = (values) => {
     
 }
 
-.form-input-box{
-    padding-left: 0;
-    padding-right: 0;
-    border-bottom: 1px solid $border-color;
-    margin-bottom: 40px;
-    &::after{
-        display: none;
+.model-icon-box{
+    margin-top: 100px;
+    display: flex;
+    width: 100%;
+    justify-content: center;
+    gap:120px;
+    .model-item-icon{
+        width:80px;
+        height:80px;
+        border-radius: 50%;
+        box-shadow: 0px 2px 12px 0px #0000001A;
+        box-sizing: border-box;
+        padding:16px;
+        img{
+            width:100%;
+        }
     }
 }
-
 .mobile-bot-text{
     margin-top: 200px;
     text-align: center;
@@ -148,17 +269,6 @@ const onSubmit = (values) => {
             }
             
         }
-        .form-input-box{
-            padding-left: 0;
-            padding-right: 0;
-            border: 1px solid $border-color;
-            border-radius: 4px;
-            padding: 12px 20px;
-            margin-bottom: 40px;
-            &::after{
-                display: none;
-            }
-        }
 
         .pad-img{
             display: block;
@@ -173,5 +283,23 @@ const onSubmit = (values) => {
     .mobile-bot-text{
         display: none;
     }
+    .model-icon-box{
+        margin-top: 50px;
+        display: flex;
+        width: 100%;
+        justify-content: center;
+        gap:60px;
+        .model-item-icon{
+            width:40px;
+            height:40px;
+            border-radius: 50%;
+            box-shadow: 0px 2px 12px 0px #0000001A;
+            box-sizing: border-box;
+            padding:6px;
+            img{
+                width:100%;
+            }
+        }
+    }
 }
 </style>

+ 104 - 0
src/views/login/EmailModel.vue

@@ -0,0 +1,104 @@
+<script setup>
+import {ref,computed,watch} from 'vue'
+import {_apiLogin} from '@/api/user'
+import {useLogin} from './hooks/useLogin'
+const props = defineProps({
+    activeModel:{//当前选择的登陆模式
+        type:String,
+        default:'ordinaryModel'
+    }
+})
+const { picSrc,picId,getPicCode,
+        timer,codeStr,codeCountDown,countDown,getCode
+      } = useLogin()
+watch(()=>props.activeModel,()=>{
+    if(props.activeModel==='emailModel'){
+        if(!timer){
+            getPicCode()
+            codeStr.value = '获取验证码'
+        }
+    }
+})
+
+//邮箱
+let email = ref('')
+//邮箱的图形验证码
+let emailPicCode = ref('')
+//邮件验证码
+let emailCode = ref('')
+const isEmailCountDown = computed(()=>{
+    return codeCountDown.value<60&&codeCountDown.value>0
+})
+
+const emailPattern = /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/
+function checkForm(){
+    if(isEmailCountDown.value) return 
+    //是否是重新获取验证码
+    if(codeStr.value==='重新获取'){
+        //清空图形验证码输入框,引导用户重新输入
+        getPicCode()
+        emailPicCode.value = ''
+    }
+    //检查邮箱和图形验证码是否正确
+    if(!emailPicCode.value.length){
+        showToast('请输入图形验证码')
+        return
+    }
+    if(!email.value.length){
+        showToast('请输入邮箱')
+        return 
+    }
+    if(!emailPattern.test(email.value)){
+        showToast('请输入正确的邮箱')
+        return 
+    }
+
+    getCode({
+        email:email.value,
+        picCode:emailPicCode.value
+    },'email',1)
+}
+</script>
+
+<template>
+    <van-field
+        class="form-input-box form-mobile-box"
+        v-model="email"
+        name="email"
+        placeholder="请输入邮箱"
+        :rules="[
+            { required: true, message: '请输入邮箱' },
+            {validator:(val)=>emailPattern.test(val),message:'请输入正确的邮箱'}
+        ]"
+    />
+    <van-field
+        class="form-input-box form-mobile-box"
+        v-model="emailPicCode"
+        name="emailPicCode"
+        placeholder="请输入图形验证码"
+        :rules="[{ required: true, message: '请输入图形验证码' }]"
+    >
+        <template #button>
+            <div class="pic-filed" @click="getPicCode">
+                <img :src="picSrc" />
+            </div>
+        </template>
+    </van-field>
+    <van-field
+        class="form-input-box form-mobile-box"
+        v-model="emailCode"
+        name="emailCode"
+        placeholder="请输入邮件验证码"
+        :rules="[{ required: true, message: '请输入邮件验证码' }]"
+    >
+        <template #button>
+            <span class="btn-text" :class="{'disabled':isEmailCountDown}"
+                @click="checkForm"
+            >{{codeStr}}</span>
+        </template>
+    </van-field>
+</template>
+
+<style scoped lang="scss">
+@import "./style/common.scss";
+</style>

+ 389 - 0
src/views/login/ForgetPassModel.vue

@@ -0,0 +1,389 @@
+<script setup>
+import {ref,onMounted} from 'vue'
+import { showToast } from 'vant'
+import {_apiLogin} from '@/api/user'
+import {useLogin} from './hooks/useLogin'
+import {Base64} from 'js-base64'
+const props = defineProps({
+    autoAccount:{//自动填入账号,如果有
+        type:String,
+        default:''
+    }
+})
+
+const { picSrc,picId,getPicCode} = useLogin()
+
+const stepTitle = [
+    '忘记密码',
+    '安全验证',
+    '请输入验证码',
+    '设置密码'
+]
+let currentStep = ref(0)
+let account = ref('')
+let accountCode = ref('') //账号的图形验证码
+
+let userMobile = ref('')
+let areaCode = ref('')
+let userEmail = ref('')
+let choosedWay = ref('')
+
+let captInput = ref('')
+let showKeyboard = ref(false)
+function getUserInfo(values){
+    _apiLogin.apiAccountCheck({
+        CaptchaId:picId.value,
+        CaptchaCode:accountCode.value,
+        UserName:account.value
+    }).then(res=>{
+        if(res.Ret!==200){
+            //刷新图形验证码
+            getPicCode()
+            accountCode.value = ''
+            return 
+        }
+        if(res.Data){
+            const {Mobile,Email,TelAreaCode} = res.Data
+            userMobile.value = Mobile
+            userEmail.value = Email
+            areaCode.value = TelAreaCode
+            goNextStep()
+        }
+    })
+}
+
+//获取手机/邮箱验证码
+async function getVerifyCode(){
+    if(!choosedWay.value){
+        showToast('请选择验证方式')
+        return
+    }
+    const res = await _apiLogin.apiGetVerifyCode({
+            VerifyType:choosedWay.value==='mobile'?1:2,
+            CaptchaId:picId.value,
+            CaptchaCode:accountCode.value,
+            Mobile:choosedWay.value==='mobile'?userMobile.value:'',
+            Email:choosedWay.value==='email'?userEmail.value:'',
+            TelAreaCode:choosedWay.value==='mobile'?areaCode.value+'':'',
+            Source:3
+    })
+    if(res.Ret!==200) return 
+    showToast('验证码已发送')
+    goNextStep()
+}
+//检查输入的验证码
+function checkInput(){
+    if(captInput.value.length!==6){
+        showToast('请输入完整的验证码')
+        return
+    }
+    _apiLogin.apiCheckCodeVerify({
+        FindType:choosedWay.value==='mobile'?1:2,
+        VerifyCode:captInput.value,
+        UserName:account.value,
+        Mobile:choosedWay.value==='mobile'?userMobile.value:'',
+        Email:choosedWay.value==='email'?userEmail.value:''
+    }).then(res=>{
+        if(res.Ret!==200) return 
+        goNextStep()
+    })
+}
+
+//重置密码
+let checkPassStr = ref('确定')
+let resetPassTimer = 0
+let countDownNum = ref(5)
+let password = ref('')
+let checkPassword = ref('')
+
+//验证密码的正则 产品定的规则是:8位及以上,包含数字、大写字母、小写字母、特殊字符中的三个类型
+const patternPassWord = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9]).{8,}$/
+function checkPwd(pwd){
+    let num = 0
+    const patternArr = [
+        /^(?=.*[0-9])/,
+        /^(?=.*[a-z])/,
+        /^(?=.*[A-Z])/,
+        /^(?=.*[@#$%^&+=.])/,
+    ]
+    patternArr.forEach(pattern=>{
+        if(pattern.test(pwd)){
+            num++
+        }
+    })
+    if(pwd.length<8){
+        num = 0
+    }
+    return num>=3
+}
+
+function resetPass(values){
+    if(checkPassStr.value.includes('去登陆')){
+        emit('changeModel')
+        return
+    }
+    _apiLogin.resetPass({
+        UserName:account.value,
+        Password:Base64.encode(values.password),
+        RePassword:Base64.encode(values.checkPassword)
+    }).then(res=>{
+        if(res.Ret!==200) return 
+        showToast('重置密码成功,请登陆')
+        resetPassCountDown()
+    })
+}
+function resetPassCountDown(){
+    countDownNum.value--
+    checkPassStr.value = `去登陆(${countDownNum.value}s)`
+    if(countDownNum.value<=0){
+        clearInterval(resetPassTimer)
+        emit('changeModel')
+        return
+    }
+}
+function goNextStep(){
+    currentStep.value++
+}
+
+const emit = defineEmits(['changeModel'])
+function changeModel(){
+    if(currentStep.value > 0){
+        currentStep.value--
+        if(currentStep.value===0){
+            getPicCode()
+            accountCode.value = ''
+        }
+    }else{
+        emit('changeModel')
+    }
+}
+onMounted(()=>{
+    account.value = props.autoAccount
+    getPicCode()
+})
+</script>
+
+<template>
+    <div class="forget-pass-model-wrap">
+        <div class="header-nav" @click="changeModel">
+            <span><van-icon name="arrow-left" /></span>
+            <span>{{stepTitle[currentStep]}}</span>
+        </div>
+        <div class="step-container" v-show="currentStep===0">
+            <van-form @submit="getUserInfo">
+                <van-field
+                    class="form-input-box form-mobile-box"
+                    v-model="account"
+                    name="account"
+                    placeholder="请输入账号"
+                    :rules="[{ required: true, message: '请输入账号' },]"
+                />
+                <van-field
+                    class="form-input-box form-mobile-box"
+                    v-model="accountCode"
+                    name="accountCode"
+                    placeholder="请输入图形验证码"
+                    :rules="[{ required: true, message: '请输入图形验证码' }]"
+                >
+                    <template #button>
+                        <div class="pic-filed" @click="getPicCode">
+                            <img :src="picSrc" />
+                        </div>
+                    </template>
+                </van-field>
+                <div class="btn-box">
+                    <van-button 
+                        round 
+                        block 
+                        type="primary" 
+                        native-type="submit"
+                    >下一步</van-button>
+                </div>
+            </van-form>
+        </div>
+        <div class="step-container" v-show="currentStep>0">
+            <div class="container-inner" v-show="currentStep===1">
+                <div class="verification-item" 
+                    :class="{'active':choosedWay==='mobile','disabled':!userMobile.length}" @click="choosedWay = 'mobile'">
+                    <div class="icon">
+                        <img src="@/assets/imgs/login/phone-icon.png" />
+                    </div>
+                    <div class="text">{{userMobile||'暂未绑定'}}</div>
+                </div>
+                <div class="verification-item"
+                    :class="{'active':choosedWay==='email','disabled':!userEmail.length}" @click="choosedWay = 'email'">
+                    <div class="icon">
+                        <img src="@/assets/imgs/login/email-icon.png" />
+                    </div>
+                    <div class="text">{{userEmail||'暂未绑定'}}</div>
+                </div>
+                <van-button round block type="primary"
+                    @click="getVerifyCode"
+                    >获取验证码</van-button>
+            </div>
+            <div class="container-inner" v-show="currentStep===2">
+                <van-password-input
+                    :value="captInput"
+                    :length="6"
+                    :gutter="10"
+                    :mask="false"
+                    :focused="showKeyboard"
+                    @focus="showKeyboard = true"
+                    />
+                <van-number-keyboard
+                    v-model="captInput"
+                    :show="showKeyboard"
+                    @blur="showKeyboard = false"
+                    />
+                <van-button round block type="primary"
+                    @click="checkInput"
+                    >下一步</van-button>
+            </div>
+            <div class="container-inner" v-show="currentStep===3">
+                <van-form @submit="resetPass">
+                    <van-field
+                        class="form-input-box form-mobile-box"
+                        v-model="password"
+                        name="password"
+                        type="password"
+                        placeholder="请输入密码"
+                        :rules="[
+                            { required: true, message: '请输入密码' },
+                            { validator:(val)=>{
+                                if(!checkPwd(val)){
+                                    return `密码要求8位及以上,包含数字、大写字母、小写字母、特殊字符中的三个类型`
+                                }else{
+                                    return true
+                                }
+                             }
+                            }]"
+                    />
+                    <van-field
+                        class="form-input-box form-mobile-box"
+                        v-model="checkPassword"
+                        name="checkPassword"
+                        type="password"
+                        placeholder="请输入确认密码"
+                        :rules="[
+                            { required: true, message: '请输入确认密码' },
+                            { validator:(val)=>{
+                                if(val!==password){
+                                    return `两次密码输入不一致,请检查`
+                                }else{
+                                    return true
+                                }
+                             }
+                            }]"
+                    />
+                    <van-button round block type="primary"
+                        native-type="submit"
+                        >{{checkPassStr}}</van-button>
+                </van-form>
+            </div>
+        </div>
+    </div>
+</template>
+
+<style scoped lang="scss">
+@import "./style/common.scss";
+.forget-pass-model-wrap{
+    .header-nav{
+        margin-top:160px;
+        font-size: 48px;
+    }
+    .step-container{
+        margin-top: 70px;
+        .container-inner{
+            .verification-item{
+                display: flex;
+                align-items: center;
+                width:100%;
+                box-sizing: border-box;
+                padding:20px 30px;
+                margin-bottom: 60px;
+                border: 1px solid #DCDFE6;
+                border-radius: 16px;
+                background-color: #fff;
+                &.active{
+                    border: 1px solid #0052D9;
+                    background-color: #F2F3FF;
+                }
+                &.disabled{
+                    user-select: none;
+                    pointer-events: none;
+                }
+                .icon{
+                    width:80px;
+                    height: 80px;
+                    padding:16px;
+                    border-radius: 50%;
+                    box-shadow: 0px 2px 12px 0px #0000001A;
+                    margin-right:20px;
+                    img{
+                        width: 100%;
+                    }
+                }
+            }
+            .van-password-input{
+                margin-bottom: 60px;
+                :deep(.van-password-input__security){
+                    li{
+                        border: 1px solid #DCDFE6;
+                        border-radius: 8px;
+                    }
+                }
+            }
+        }
+    }
+    @media screen and (min-width:$media-width) {
+        .header-nav{
+            margin-top:0;
+            font-size: 36px;
+        }
+        .step-container{
+            margin-top: 70px;
+            .container-inner{
+                .verification-item{
+                    display: flex;
+                    align-items: center;
+                    width:100%;
+                    box-sizing: border-box;
+                    padding:10px 15px;
+                    margin-bottom: 30px;
+                    border: 1px solid #DCDFE6;
+                    border-radius: 8px;
+                    background-color: #fff;
+                    &.active{
+                        border: 1px solid #0052D9;
+                        background-color: #F2F3FF;
+                    }
+                    &.disabled{
+                        user-select: none;
+                        pointer-events: none;
+                    }
+                    .icon{
+                        width:40px;
+                        height: 40px;
+                        padding:8px;
+                        border-radius: 50%;
+                        box-shadow: 0px 2px 12px 0px #0000001A;
+                        margin-right:20px;
+                        img{
+                            width: 100%;
+                        }
+                    }
+                }
+                .van-password-input{
+                    margin-bottom: 60px;
+                    :deep(.van-password-input__security){
+                        li{
+                            border: 1px solid #DCDFE6;
+                            border-radius: 8px;
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+</style>

+ 146 - 0
src/views/login/MobileModel.vue

@@ -0,0 +1,146 @@
+<script setup>
+import {ref,computed,watch} from 'vue'
+import { showToast } from 'vant';
+import { _apiLogin } from '@/api/user'
+import { useLogin } from './hooks/useLogin'
+import { isMobile } from 'vant/lib/utils';
+
+
+const props = defineProps({
+    activeModel:{//当前选择的登陆模式
+        type:String,
+        default:'ordinaryModel'
+    }
+})
+
+const { picSrc,picId,getPicCode,
+        timer,codeStr,codeCountDown,countDown,getCode
+    } = useLogin()
+
+watch(()=>props.activeModel,()=>{
+    if(props.activeModel==='mobileModel'){
+        if(!timer){
+            getPicCode()
+            codeStr.value = '获取验证码'
+        }
+    }
+})
+
+//手机号
+let mobile = ref('')
+//图形验证码
+let mobilePicCode = ref('')
+//短信验证码
+let mobileCode = ref('')
+
+//短信验证码倒计时相关
+const isMobileCountDown = computed(()=>{
+    return codeCountDown.value<60&&codeCountDown.value>0
+})
+
+//区号相关
+let mobileTelAreaCode=ref('86')
+let showMobilePicker = ref(false)
+let TelAreaCodeList = ref([])
+//获取区号
+function getTelAreaCode(){
+    _apiLogin.getPhoneAreaCode().then(res=>{
+        if(res.Ret!==200) return 
+        TelAreaCodeList.value = res.Data||[]
+    })
+}
+getTelAreaCode()
+
+//校验表单
+function checkForm(){
+    if(isMobileCountDown.value) return 
+    //是否是重新获取验证码
+    if(codeStr.value==='重新获取'){
+        //清空图形验证码输入框,引导用户重新输入
+        getPicCode()
+        mobilePicCode.value = ''
+    }
+    //检查手机号和图形验证码是否正确
+    if(!mobilePicCode.value.length){
+        showToast('请输入图形验证码')
+        return
+    }
+    if(!mobile.value.length){
+        showToast('请输入手机号')
+        return 
+    }
+    if(!isMobile(mobile.value)){
+        showToast('请输入正确的手机号')
+        return 
+    }
+    getCode({ mobile:mobile.value,
+              areaCode:mobileTelAreaCode.value,
+              picCode:mobilePicCode.value
+            },'mobile',1)
+
+}
+
+defineExpose({
+    getPicCode
+})
+</script>
+
+<template>
+    <van-field
+        class="form-input-box form-mobile-box tel"
+        v-model="mobile"
+        type="tel"
+        name="mobile"
+        placeholder="请输入手机号"
+        :rules="[
+            { required: true, message: '请输入手机号' },
+            { validator:(val)=>isMobile(val),message:'请输入正确的手机号'}
+        ]"
+    >
+        <!-- 选择区号的选择框-->
+        <template #button>
+            <div class="area-filed" @click="showMobilePicker = true" >
+                <span>+{{mobileTelAreaCode}}</span>
+                <van-icon name="arrow-down" />
+            </div>
+        </template>
+    </van-field>
+    <van-popup v-model:show="showMobilePicker" position="bottom">
+        <van-picker
+            :columns-field-names="{ text: 'Name',value: 'Value',}"
+            :columns="TelAreaCodeList"
+            @confirm="({selectedValues})=>{mobileTelAreaCode= selectedValues[0];showMobilePicker = false}"
+            @cancel="showMobilePicker = false"
+        />
+    </van-popup>
+    <van-field
+        class="form-input-box form-mobile-box"
+        v-model="mobilePicCode"
+        name="picCode"
+        placeholder="请输入图形验证码"
+        :rules="[{ required: true, message: '请输入图形验证码' }]"
+    >
+        <template #button>
+            <div class="pic-filed" @click="getPicCode">
+                <img :src="picSrc" />
+            </div>
+        </template>
+    </van-field>
+    <van-field
+        class="form-input-box form-mobile-box"
+        v-model="mobileCode"
+        name="mobileCode"
+        placeholder="请输入短信验证码"
+        :rules="[{ required: true, message: '请输入短信验证码' }]"
+    >
+        <template #button>
+            <span class="btn-text" :class="{'disabled':isMobileCountDown}"
+                @click="checkForm"
+            >{{codeStr}}</span>
+        </template>
+    </van-field>
+</template>
+
+<style scoped lang="scss">
+@import "./style/common.scss";
+</style>

+ 45 - 0
src/views/login/OrdinaryModel.vue

@@ -0,0 +1,45 @@
+<script setup>
+import {Base64} from 'js-base64'
+import {ref} from 'vue'
+const props = defineProps({
+    activeModel:{//当前选择的登陆模式
+        type:String,
+        default:'ordinaryModel'
+    }
+})
+
+//账号密码
+let username=ref(localStorage.getItem('account')?Base64.decode(localStorage.getItem('account')):'')
+let password=ref(localStorage.getItem('checkPass')?Base64.decode(localStorage.getItem('checkPass')):'')
+
+const emit = defineEmits(['changeModel'])
+function changeModel(){
+    emit('changeModel')
+}
+</script>
+
+<template>
+    <van-field
+        class="form-input-box"
+        v-model="username"
+        name="username"
+        placeholder="请输入管理后台账号"
+        :rules="[{ required: true, message: '请填写账号' }]"
+    />
+    <van-field
+        class="form-input-box"
+        v-model="password"
+        type="password"
+        name="password"
+        placeholder="请输入密码"
+        :rules="[{ required: true, message: '请填写密码' }]"
+    >
+        <template #button>
+            <span class="btn-text" style="color:#0052D9;" @click="changeModel">忘记密码</span>
+        </template>
+    </van-field>
+</template>
+
+<style scoped lang="scss">
+@import "./style/common.scss";
+</style>

+ 60 - 0
src/views/login/hooks/useLogin.js

@@ -0,0 +1,60 @@
+import {ref} from 'vue'
+import { showToast } from 'vant';
+import {_apiLogin} from '@/api/user'
+
+export function useLogin(){
+    //图形验证码src
+    let picSrc = ref('')
+    //图形验证码id
+    let picId = ref('')
+
+    //获取图形验证码
+    async function getPicCode(){
+        const res = await _apiLogin.apiGetPicCode()
+        if(res.Ret!==200) return
+        picId.value = res.Data.Id
+        picSrc.value = res.Data.Base64Blob
+    }
+
+    async function getCode({mobile,email,picCode,areaCode},model,source){
+        const res = await _apiLogin.apiGetVerifyCode({
+            VerifyType:model==='mobile'?1:2,
+            CaptchaId:picId.value,
+            CaptchaCode:picCode,
+            Mobile:mobile||'',
+            Email:email||'',
+            TelAreaCode:areaCode+''||'',
+            Source:source
+        })
+        if(res.Ret!==200) return 
+        showToast('验证码已发送')
+        //60秒倒计时
+        countDown()
+        timer = setInterval(()=>{
+            countDown()
+        },1000)
+    }
+    let timer = 0
+    let codeStr = ref('获取验证码')
+    let codeCountDown = ref(60)
+    function countDown(){
+        codeCountDown.value--
+        codeStr.value = `重新获取(${codeCountDown.value})秒`
+        if(codeCountDown.value<=0){
+            clearInterval(timer)
+            codeStr.value = '重新获取'
+        }
+    }
+
+
+
+    return {
+        picId,
+        picSrc,
+        getPicCode,
+
+        timer,codeStr,codeCountDown,
+        countDown,
+        getCode,
+    }
+}

+ 80 - 0
src/views/login/style/common.scss

@@ -0,0 +1,80 @@
+.form-input-box{
+    padding-left: 0;
+    padding-right: 0;
+    border-bottom: 1px solid $border-color;
+    margin-bottom: 40px;
+    &::after{
+        display: none;
+    }
+}
+.form-mobile-box{
+    &.tel{
+        :deep(.van-field__body){
+            flex-direction: row-reverse;
+        }
+        
+    }
+    .area-filed{
+        width:110px;
+        text-align: left;
+    }
+    .pic-filed{
+        width:180px;
+        height: 60px;
+        box-sizing: border-box;
+        padding: 0 15px;
+        background-color:#D9D9D9;
+        img{
+            width:100%;
+            height: 100%;
+        }
+    }
+    .btn-text{
+        color: #0052D9;
+        &.disabled{
+            color:#999999;
+        }
+    }
+}
+@media screen and (min-width:$media-width) {
+    .form-input-box{
+        padding-left: 0;
+        padding-right: 0;
+        border: 1px solid $border-color;
+        border-radius: 4px;
+        padding: 12px 20px;
+        margin-bottom: 40px;
+        &::after{
+            display: none;
+        }
+    }
+    .form-mobile-box{
+        &.tel{
+            :deep(.van-field__body){
+                flex-direction: row-reverse;
+            }
+            
+        }
+        .area-filed{
+            width:110px;
+            text-align: left;
+        }
+        .pic-filed{
+            width:80px;
+            height: 24px;
+            box-sizing: border-box;
+            padding: 0 15px;
+            background-color:#D9D9D9;
+            img{
+                width:100%;
+                height: 100%;
+            }
+        }
+        .btn-text{
+            color: #0052D9;
+            &.disabled{
+                color:#999999;
+            }
+        }
+    }
+}