Login.vue 8.2 KB
<!--
 * @Date: 2025-11-10 18:08:59
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2025-11-11 13:31:30
 * @FilePath: /stdj_h5/src/views/Login.vue
 * @Description: 登录页
-->
<template>
    <div class="login-page">
        <!-- 顶部LOGO标题 -->
        <div class="logo-title">
            <img class="logo-img" src="https://cdn.ipadbiz.cn/stdj/images/logo@2x.png" alt="Logo">
        </div>

        <!-- 戒子身份验证容器 -->
        <div class="auth-card">
            <div class="card-title">戒子身份验证</div>

            <!-- 登录表单:手机号 -->
            <div class="form-item">
                <div class="input-with-icon phone">
                    <input class="input" type="tel" placeholder="请输入手机号" v-model="phone" maxlength="11" />
                </div>
            </div>

            <!-- 登录表单:验证码 + 获取验证码 -->
            <div class="form-item">
                <div class="code-row">
                    <div class="input-with-icon code">
                        <input class="input" type="tel" placeholder="请输入验证码" v-model="code" maxlength="6" />
                    </div>
                    <div class="btn send" :class="{ disabled: send_disabled }" @click="on_click_send_sms">
                        <span v-if="countdown === 0">获取验证码</span>
                        <span v-else>{{ countdown }}s后重试</span>
                    </div>
                </div>
            </div>

            <!-- 立即验证按钮 -->
            <div class="form-item">
                <div class="btn primary" :class="{ disabled: login_disabled }" @click="on_click_login">立即验证</div>
            </div>
        </div>
    </div>
</template>

<script setup>
import { ref, onUnmounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTitle } from '@vueuse/core'
import { smsAPI } from '@/api/common.js'
import { loginAPI } from '@/api/index.js'
import { showToast, showFailToast, showSuccessToast } from 'vant'
import Cookies from 'js-cookie'

useTitle('戒子身份验证')

const route = useRoute()
const router = useRouter()

// 表单状态
const phone = ref('')
const code = ref('')
const countdown = ref(0)
const timer_id = ref(null)
const sending = ref(false)
const logging = ref(false)

/**
 * 发送验证码按钮禁用条件
 * 说明:倒计时进行中或正在发送或手机号无效时不可操作
 */
const send_disabled = computed(function () {
    return countdown.value > 0 || sending.value || !is_valid_phone(phone.value)
})

/**
 * 登录按钮禁用条件
 * 说明:登录进行中或手机号/验证码未通过校验时不可操作
 */
const login_disabled = computed(function () {
    return (
        logging.value ||
        !is_valid_phone(phone.value) ||
        !/^\d{4,6}$/.test(String(code.value || '').trim())
    )
})

/**
 * 点击发送验证码包装函数
 * 说明:在禁用态下不触发真实发送逻辑
 * @returns {void}
 */
const on_click_send_sms = function () {
    if (send_disabled.value) return
    on_send_sms()
}

/**
 * 点击登录包装函数
 * 说明:在禁用态下不触发真实登录逻辑
 * @returns {void}
 */
const on_click_login = function () {
    if (login_disabled.value) return
    on_login()
}

/**
 * 校验手机号格式
 * 说明:仅支持中国大陆11位手机号校验
 * @param {string} v 手机号
 * @returns {boolean} 是否有效
 */
const is_valid_phone = function (v) {
    return /^1\d{10}$/.test(String(v || '').trim())
}

/**
 * 启动60秒倒计时
 * 说明:发送验证码成功后启动,期间禁用发送按钮
 * @returns {void}
 */
const start_countdown = function () {
    countdown.value = 60
    if (timer_id.value) {
        clearInterval(timer_id.value)
        timer_id.value = null
    }
    timer_id.value = setInterval(function () {
        countdown.value = countdown.value - 1
        if (countdown.value <= 0) {
            clearInterval(timer_id.value)
            timer_id.value = null
            countdown.value = 0
        }
    }, 1000)
}

/**
 * 发送验证码
 * 说明:调用接口,成功后开始倒计时
 * @returns {Promise<void>}
 */
const on_send_sms = async function () {
    if (sending.value || countdown.value > 0) return
    if (!is_valid_phone(phone.value)) {
        showFailToast('请输入有效的手机号')
        return
    }
    try {
        sending.value = true
        const { code } = await smsAPI({ phone: phone.value })
        if (code) {
            showToast('验证码已发送')
            start_countdown()
        }
    } catch (e) {
        showFailToast('网络异常,请稍后重试')
    } finally {
        sending.value = false
    }
}

/**
 * 登录
 * @returns {Promise<void>}
 */
const on_login = async function () {
    if (logging.value) return
    if (!is_valid_phone(phone.value)) {
        showFailToast('请输入有效的手机号')
        return
    }
    if (!/^\d{4,6}$/.test(String(code.value || '').trim())) {
        showFailToast('请输入4~6位数字验证码')
        return
    }
    try {
        logging.value = true
        const { code, data } = await loginAPI({ phone: phone.value, code: code.value })
        if (code) {
            // 登录成功后,将token存储到cookie中
            Cookies.set('token-stdj', data.token, { expires: 7 })
            showSuccessToast('登录成功')
            // 跳转戒子详情页
            router.replace({ path: route.query.redirect })
        }
    } catch (e) {
        showFailToast('网络异常,请稍后重试')
    } finally {
        logging.value = false
    }
}

// 组件卸载时清理计时器
onUnmounted(function () {
    if (timer_id.value) {
        clearInterval(timer_id.value)
        timer_id.value = null
    }
})
</script>

<style lang="less" scoped>
// 页面背景与布局
.login-page {
    min-height: 100vh;
    background-color: #FCF8F1; // 整个页面背景色
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 6rem 1rem;
    background-image: url('https://cdn.ipadbiz.cn/stdj/images/bg002@2x.png');
    background-size: cover;
    background-position: center;
}

// 顶部Logo标题
.logo-title {
    margin-bottom: 2rem;
}
.logo-img {
    height: 5.5rem;
    object-fit: contain;
    margin-top: 1rem;
}

// 验证容器
.auth-card {
    max-width: 26rem;
    background-color: rgba(174, 155, 99, 0.14); // 容器背景色
    border-radius: 0.75rem;
    padding: 1rem;
    box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.06);
}
.card-title {
    text-align: center;
    color: #432C0E;
    font-size: 1.125rem;
    font-weight: 700;
    margin-top: 0.75rem;
    margin-bottom: 1.25rem;
}

// 表单项布局
.form-item {
    margin-top: 1.25rem;
    margin-bottom: 0.75rem;
}
.code-row {
    display: flex;
    gap: 0.5rem;
    align-items: center;
}

// 输入框:带左侧图标
.input-with-icon {
    position: relative;
}
.input-with-icon::before {
    content: '';
    position: absolute;
    left: 0.75rem;
    top: 50%;
    width: 1rem;
    height: 1rem;
    background-size: contain;
    background-position: center;
    background-repeat: no-repeat;
    transform: translateY(-50%);
}
.input-with-icon.phone::before {
    background-image: url('https://cdn.ipadbiz.cn/stdj/images/%E6%89%8B%E6%9C%BA@2x.png');
}
.input-with-icon.code::before {
    background-image: url('https://cdn.ipadbiz.cn/stdj/images/%E9%AA%8C%E8%AF%81%E7%A0%81@2x-1.png');
}

.input {
    width: 100%;
    height: 2.5rem;
    border-radius: 0.5rem;
    border: none;
    background-color: #FFFFFF;
    padding: 0 0.75rem 0 2.25rem; // 为左侧图标预留空间
    box-shadow: inset 0 0 0 0.0625rem rgba(0, 0, 0, 0.08);
}
.input::placeholder {
    color: #999999;
}

// 按钮样式
.btn {
    height: 2.5rem;
    padding: 0 0.75rem;
    border: none;
    border-radius: 0.5rem;
    background-color: #A67939; // 获取验证码与立即验证背景色
    color: #FFFFFF;
    font-weight: 600;
    text-align: center;
    line-height: 2.5rem;
}
.btn.send {
    white-space: nowrap;
}
.btn.primary {
    width: 100%;
}
.btn.disabled {
    // 不可操作态:明显的灰显与禁止样式
    opacity: 0.5;
    cursor: not-allowed;
    pointer-events: none;
    filter: grayscale(30%);
    box-shadow: none;
}
</style>