feat(auth): 实现登录注册功能和路由权限控制
- 新增 Login.vue 实现登录与注册界面,支持手机号、密码、验证码等验证 - 添加登录状态保持并在登录成功后设置 token - 修改路由配置,新增 /login 路由,并调整默认班级页路由为 /class - 移除 Header 组件中原有登录按钮,改为通过路由控制访问权限 - 实现路由前置守卫,根据 token 自动跳转登录页或班级页 - 添加验证码发送功能及倒计时禁用按钮逻辑 - 完善表单校验规则,区分登录和注册模式验证字段
This commit is contained in:
@@ -34,12 +34,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
|
||||||
<a href="#" @click.prevent="showLogin = true"
|
|
||||||
class="text-gray-800 dark:text-white hover:bg-gray-50 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 dark:hover:bg-gray-700 focus:outline-none dark:focus:ring-gray-800">
|
|
||||||
Login
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
<button data-collapse-toggle="mobile-menu-2" type="button"
|
<button data-collapse-toggle="mobile-menu-2" type="button"
|
||||||
class="inline-flex items-center p-2 ml-1 text-sm text-gray-500 rounded-lg lg:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
|
class="inline-flex items-center p-2 ml-1 text-sm text-gray-500 rounded-lg lg:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
|
||||||
aria-controls="mobile-menu-2" aria-expanded="false">
|
aria-controls="mobile-menu-2" aria-expanded="false">
|
||||||
@@ -65,7 +59,7 @@
|
|||||||
aria-current="page">Home</a>
|
aria-current="page">Home</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link to="/"
|
<router-link to="/class"
|
||||||
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 lg:hover:bg-transparent lg:border-0 lg:hover:text-primary-700 lg:p-0 dark:text-gray-400 lg:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent dark:border-gray-700">
|
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 lg:hover:bg-transparent lg:border-0 lg:hover:text-primary-700 lg:p-0 dark:text-gray-400 lg:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent dark:border-gray-700">
|
||||||
班级
|
班级
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|||||||
198
enlish-vue/src/pages/Login.vue
Normal file
198
enlish-vue/src/pages/Login.vue
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-screen h-screen overflow-hidden flex">
|
||||||
|
<div class="flex-1 bg-black">
|
||||||
|
<el-image fit="cover" src="https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg"
|
||||||
|
style="width: 100%; height: 100%" />
|
||||||
|
</div>
|
||||||
|
<div class="w-[500px] z-10 bg-white dark:bg-gray-800 p-8">
|
||||||
|
<div class="mb-4 grid grid-cols-2 gap-2 justify-center pt-64">
|
||||||
|
<button type="button" @click="switchMode('login')" class="w-full px-3 py-1.5 rounded text-sm"
|
||||||
|
:class="mode === 'login' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'">
|
||||||
|
登录
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="switchMode('register')" class="w-full px-3 py-1.5 rounded text-sm"
|
||||||
|
:class="mode === 'register' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'">
|
||||||
|
注册
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h1 class="text-2xl font-bold" v-if="mode === 'login'">登录</h1>
|
||||||
|
<h1 class="text-2xl font-bold" v-else>注册新用户</h1>
|
||||||
|
<p class="text-sm text-gray-500" v-if="mode === 'login'">请输入手机号和密码进行登录</p>
|
||||||
|
<p class="text-sm text-gray-500" v-else>请填写以下信息进行注册</p>
|
||||||
|
</div>
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" class="space-y-4">
|
||||||
|
<el-form-item prop="phone">
|
||||||
|
<el-input v-model="form.phone" maxlength="11" placeholder="手机号" />
|
||||||
|
</el-form-item>
|
||||||
|
<template v-if="mode === 'login'">
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input v-model="form.password" type="password" maxlength="20" placeholder="密码" />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<el-form-item prop="name">
|
||||||
|
<el-input v-model="form.name" maxlength="20" placeholder="姓名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input v-model="form.password" type="password" maxlength="20" placeholder="设置密码" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="password_repeat">
|
||||||
|
<el-input v-model="form.password_repeat" type="password" maxlength="20" placeholder="重复密码" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item prop="code">
|
||||||
|
<div class="flex items-center gap-2 w-full">
|
||||||
|
<el-input v-model="form.code" maxlength="6" placeholder="验证码" />
|
||||||
|
<el-button type="primary" @click="sendCode" :disabled="codeDisabled || !form.phone">
|
||||||
|
{{ codeBtnText }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
</el-form>
|
||||||
|
<div class="mt-6">
|
||||||
|
<el-button :type="mode === 'login' ? 'success' : 'warning'" plain class="w-full" :disabled="loading"
|
||||||
|
@click="userLogin">
|
||||||
|
<span v-if="!loading && mode === 'login'">立即登录</span>
|
||||||
|
<span v-if="loading && mode === 'login'">登录中...</span>
|
||||||
|
<span v-if="!loading && mode === 'register'">立即注册</span>
|
||||||
|
<span v-if="loading && mode === 'register'">处理中...</span>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-2 left-6 text-white drop-shadow">
|
||||||
|
<h2 class="text-xl">欢迎来到智慧英语</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import router from '@/router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { login, getVerificationCode } from '@/api/user'
|
||||||
|
import { setToken } from '@/composables/auth'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const formRef = ref()
|
||||||
|
const mode = ref('login')
|
||||||
|
const codeDisabled = ref(false)
|
||||||
|
const codeBtnText = ref('发送验证码')
|
||||||
|
let timer = null
|
||||||
|
const form = reactive({
|
||||||
|
phone: '',
|
||||||
|
name: '',
|
||||||
|
password: '',
|
||||||
|
password_repeat: '',
|
||||||
|
code: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
phone: [
|
||||||
|
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||||
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入合法的手机号', trigger: ['blur', 'change'] },
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ min: 6, max: 20, message: '密码长度为6-20位', trigger: ['blur', 'change'] },
|
||||||
|
],
|
||||||
|
name: [
|
||||||
|
{
|
||||||
|
validator: (_r, v, cb) => {
|
||||||
|
if (mode.value !== 'register') return cb()
|
||||||
|
if (!v?.trim()) return cb(new Error('请输入姓名'))
|
||||||
|
cb()
|
||||||
|
}, trigger: ['blur', 'change']
|
||||||
|
},
|
||||||
|
],
|
||||||
|
password_repeat: [
|
||||||
|
{
|
||||||
|
validator: (_r, v, cb) => {
|
||||||
|
if (mode.value !== 'register') return cb()
|
||||||
|
if (!v) return cb(new Error('请再次输入密码'))
|
||||||
|
if (v !== form.password) return cb(new Error('两次密码不一致'))
|
||||||
|
cb()
|
||||||
|
}, trigger: ['blur', 'change']
|
||||||
|
},
|
||||||
|
],
|
||||||
|
code: [
|
||||||
|
{
|
||||||
|
validator: (_r, v, cb) => {
|
||||||
|
if (mode.value !== 'register') return cb()
|
||||||
|
if (!v?.trim()) return cb(new Error('请输入验证码'))
|
||||||
|
cb()
|
||||||
|
}, trigger: ['blur', 'change']
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
async function userLogin() {
|
||||||
|
if (loading.value) return
|
||||||
|
formRef.value?.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const payload = mode.value === 'login'
|
||||||
|
? {
|
||||||
|
phone: form.phone.trim(),
|
||||||
|
password: form.password.trim(),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
phone: form.phone.trim(),
|
||||||
|
name: form.name.trim(),
|
||||||
|
password: form.password.trim(),
|
||||||
|
code: form.code.trim(),
|
||||||
|
}
|
||||||
|
const res = await login(payload)
|
||||||
|
const data = res.data
|
||||||
|
if (data?.success) {
|
||||||
|
if (mode.value === 'login') {
|
||||||
|
try { setToken(data.data) } catch { }
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
router.push('/class')
|
||||||
|
} else {
|
||||||
|
ElMessage.success('注册成功')
|
||||||
|
mode.value = 'login'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ElMessage.error(data?.message || '登录失败')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchMode(m) {
|
||||||
|
mode.value = m
|
||||||
|
formRef.value?.clearValidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendCode() {
|
||||||
|
if (!form.phone || codeDisabled.value) return
|
||||||
|
codeDisabled.value = true
|
||||||
|
try {
|
||||||
|
await getVerificationCode({ phone: form.phone.trim() })
|
||||||
|
ElMessage.success('验证码已发送')
|
||||||
|
let count = 60
|
||||||
|
codeBtnText.value = `${count}s`
|
||||||
|
timer = setInterval(() => {
|
||||||
|
count -= 1
|
||||||
|
if (count <= 0) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
codeDisabled.value = false
|
||||||
|
codeBtnText.value = '发送验证码'
|
||||||
|
} else {
|
||||||
|
codeBtnText.value = `${count}s`
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
} catch {
|
||||||
|
codeDisabled.value = false
|
||||||
|
codeBtnText.value = '发送验证码'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -2,12 +2,22 @@ import router from '@/router/index'
|
|||||||
|
|
||||||
import { showMessage } from '@/composables/util'
|
import { showMessage } from '@/composables/util'
|
||||||
import { showPageLoading, hidePageLoading } from '@/composables/util'
|
import { showPageLoading, hidePageLoading } from '@/composables/util'
|
||||||
|
import { getToken } from '@/composables/auth'
|
||||||
|
|
||||||
// 全局路由前置守卫
|
// 全局路由前置守卫
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
console.log('==> 全局路由前置守卫')
|
console.log('==> 全局路由前置守卫')
|
||||||
// 展示页面加载 Loading
|
// 展示页面加载 Loading
|
||||||
showPageLoading()
|
showPageLoading()
|
||||||
|
const token = getToken()
|
||||||
|
if (!token && to.path !== '/login') {
|
||||||
|
next({ path: '/login' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (token && to.path === '/login') {
|
||||||
|
next({ path: '/class' })
|
||||||
|
return
|
||||||
|
}
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import { createRouter, createWebHashHistory } from 'vue-router'
|
|||||||
import Admid from '@/pages/admid/admid.vue'
|
import Admid from '@/pages/admid/admid.vue'
|
||||||
import Student from '@/pages/student.vue'
|
import Student from '@/pages/student.vue'
|
||||||
import PlanTTS from '@/pages/PlanTTS.vue'
|
import PlanTTS from '@/pages/PlanTTS.vue'
|
||||||
|
import Login from '@/pages/Login.vue'
|
||||||
|
|
||||||
// 统一在这里声明所有路由
|
// 统一在这里声明所有路由
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/', // 路由地址
|
path: '/class',
|
||||||
component: Class, // 对应组件
|
component: Class,
|
||||||
meta: { // meta 信息
|
meta: {
|
||||||
title: '班级' // 页面标题
|
title: '班级'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -43,6 +44,13 @@ const routes = [
|
|||||||
title: '管理员页面' // 页面标题
|
title: '管理员页面' // 页面标题
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
component: Login,
|
||||||
|
meta: {
|
||||||
|
title: '登录'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/plan/tts',
|
path: '/plan/tts',
|
||||||
component: PlanTTS,
|
component: PlanTTS,
|
||||||
|
|||||||
Reference in New Issue
Block a user