目录结构
├── views/login
│ ├── ├── c-cpns
│ ├── ├── ├── login-panel.vue
│ ├── ├── ├── pane-account.vue
│ ├── ├── ├── pane-phone.vue
│ ├── ├── Login.vue
组件封装LoginPanel
1.Tabs 标签页
Tabs 属性
- type 设置为 border-card设置标签页为带有边框的卡片
- v-model选中选项卡的 name
- strech 标签的宽度是否自撑开
Tab-pane 属性
- name:与选项卡绑定值 value 对应的标识符,表示选项卡别名
Tab-pane 插槽
- label Tab-pane 的标题内容
2.Icon 图标
安装npm install @element-plus/icons-vue
全局注册
3.Checkbox 多选框
Checkbox 属性
- v-model:选中项绑定值
- label:选中状态的值
- size:尺寸
4.Link 链接
type:类型primary / success / warning / danger / info / default
<template>
<div class="login-panel">
<!-- 顶部的标题 -->
<h1 class="title">弘源后台管理系统</h1>
<!-- 中间的tabs切换 -->
<div class="tabs">
<el-tabs type="border-card" stretch v-model="activeName">
<!-- 1.账号登录的Pane -->
<el-tab-pane name="account">
<template #label>
<div class="label">
<el-icon><UserFilled /></el-icon>
<span class="text">帐号登录</span>
</div>
</template>
<pane-account ref="accountRef" />
</el-tab-pane>
<!-- 2.手机登录的Pane -->
<el-tab-pane name="phone">
<template #label>
<div class="label">
<el-icon><Cellphone /></el-icon>
<span class="text">手机登录</span>
</div>
</template>
<pane-phone />
</el-tab-pane>
</el-tabs>
</div>
<!-- 底部区域 -->
<div class="controls">
<el-checkbox v-model="isRemPwd" label="记住密码" size="large" />
<el-link type="primary">忘记密码</el-link>
</div>
<el-button
class="login-btn"
type="primary"
size="large"
@click="handleLoginBtnClick"
>
立即登录
</el-button>
</div>
</template>
<script setup lang="ts">
import { localCache } from '@/utils/cache'//localStorage封装
import { ref, watch } from 'vue'
import PaneAccount from './pane-account.vue'
import PanePhone from './pane-phone.vue'
const activeName = ref('account')
//??先检查左侧表达式的非空值,若非空则返回,若空则返回右侧表达式的值
const isRemPwd = ref<boolean>(localCache.getCache('isRemPwd') ?? false)
watch(isRemPwd, (newValue) => {
localCache.setCache('isRemPwd', newValue)
})
const accountRef = ref<InstanceType<typeof PaneAccount>>()
function handleLoginBtnClick() {
if (activeName.value === 'account') {
accountRef.value?.loginAction(isRemPwd.value)//子组件定义的方法,进行表单校验
} else {
console.log('用户在进行手机登录')
}
}
</script>
<style lang="less" scoped>
.login-panel {
width: 330px;
margin-bottom: 150px;
.title {
text-align: center;
margin-bottom: 15px;
}
.label {
display: flex;
align-items: center;
justify-content: center;
.text {
margin-left: 5px;
}
}
.controls {
margin-top: 12px;
display: flex;
justify-content: space-between;
}
.login-btn {
margin-top: 10px;
width: 100%;
// --el-button-size: 50px;
}
}
</style>
组件封装PaneAccount
1.Form 表单
表单校验:添加rules和prop属性,并设置相应规则
Form 属性
- model:表单数据对象,双向绑定数据
- rules:表单验证规则
- label-width:标签的长度
- size:用于控制该表单内组件的尺寸
- status-icon:是否在输入框中显示校验结果反馈图标
Form Item 属性
- prop:model 的键名,在定义了 validate、resetFields 的方法时,该属性是必填的
- label:标签文本
2.Input输入框
input 属性
- v-model:绑定值
- shoe-password:是否显示切换密码图标
<template>
<div class="pane-account">
<el-form
:model="account"
:rules="accountRules"
label-width="60px"
size="large"
status-icon
ref="formRef"
>
<el-form-item label="帐号" prop="name">
<el-input v-model="account.name" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="account.password" show-password />
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormRules, ElForm } from 'element-plus'
import useLoginStore from '@/store/login/login'
import type { IAccount } from '@/types'
import { localCache } from '@/utils/cache'
const CACHE_NAME = 'name'
const CACHE_PASSWORD = 'password'
// 1.定义account数据
const account = reactive<IAccount>({
name: localCache.getCache(CACHE_NAME) ?? '',
password: localCache.getCache(CACHE_PASSWORD) ?? ''
})
// 2.定义校验规则
const accountRules: FormRules = {
name: [
{ required: true, message: '必须输入帐号信息~', trigger: 'blur' },
{
pattern: /^[a-z0-9]{6,20}$/,
message: '必须是6~20数字或字母组成~',
trigger: 'blur'
}
],
password: [
{ required: true, message: '必须输入密码信息~', trigger: 'blur' },
{
pattern: /^[a-z0-9]{3,}$/,
message: '必须是3位以上数字或字母组成',
trigger: 'blur'
}
]
}
// 3.执行帐号的登录逻辑:校验成功后进行登录并进行相应的缓存
const formRef = ref<InstanceType<typeof ElForm>>()
const loginStore = useLoginStore()//登录相关store
function loginAction(isRemPwd: boolean) {
formRef.value?.validate((valid) => {
if (valid) {
// 1.获取用户输入的帐号和密码
const name = account.name
const password = account.password
// 2.向服务器发送网络请求(携带账号和密码)
loginStore.loginAccountAction({ name, password }).then(() => {
// 3.判断是否需要记住密码
if (isRemPwd) {//父传子
localCache.setCache(CACHE_NAME, name)
localCache.setCache(CACHE_PASSWORD, password)
} else {
localCache.removeCache(CACHE_NAME)
localCache.removeCache(CACHE_PASSWORD)
}
})
} else {
ElMessage.error('Oops, 请您输入正确的格式后再操作~~.')
}
})
}
defineExpose({
loginAction
})
</script>
<style lang="less" scoped>
.pane-account {
color: red;
}
</style>
组件封装PanePhone
<template>
<div class="panel-phone">
<el-form label-width="60px" size="large">
<el-form-item label="手机号">
<el-input />
</el-form-item>
<el-form-item label="验证码">
<div class="verify-code">
<el-input />
<el-button class="get-btn" type="primary">获取验证码</el-button>
</div>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped>
.panel-phone {
color: red;
}
.verify-code {
display: flex;
.get-btn {
margin-left: 8px;
}
}
</style>
login页面
<template>
<div class="login">
<login-panel />
</div>
</template>
<script setup lang="ts">
import LoginPanel from './c-cpns/login-panel.vue'
</script>
<style lang="less" scoped>
.login {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background: url('../../assets/img/login-bg.svg');
}
</style>
登录逻辑实现
API封装
//service/login/login.ts
import hyRequest from '..'
import type { IAccount } from '@/types'
//export interface IAccount {
// name: string
// password: string
//}
//登录接口 id/token
export function accountLoginRequest(account: IAccount) {
return hyRequest.post({
url: '/login',
data: account
})
}
//根据id获取用户详细信息
export function getUserInfoById(id: number) {
return hyRequest.get({
url: `/users/${id}`
})
}
//获取登录用户的菜单树
export function getUserMenusByRoleId(id: number) {
return hyRequest.get({
url: `/role/${id}/menu`
})
}
登录功能实现
//store/login/login.ts
import { defineStore } from 'pinia'
import {
accountLoginRequest,
getUserInfoById,
getUserMenusByRoleId
} from '@/service/login/login'
import type { IAccount } from '@/types'
import { localCache } from '@/utils/cache'//本地缓存封装
import { mapMenusToPermissions, mapMenusToRoutes } from '@/utils/map-menus'//根据菜单判断用户权限
import router from '@/router'
import { LOGIN_TOKEN } from '@/global/constants'
//export const LOGIN_TOKEN = 'login/token'
import useMainStore from '../main/main'
interface ILoginState {
token: string
userInfo: any
userMenus: any
permissions: string[]
}
const useLoginStore = defineStore('login', {
// 如何制定state的类型
state: (): ILoginState => ({
token: localCache.getCache(LOGIN_TOKEN)??'',
userInfo: localCache.getCache('userInfo')??{},
userMenus: localCache.getCache('userMenu')??[],
permissions: []
}),
actions: {
async loginAccountAction(account: IAccount) {
// 1.账号登录, 获取token等信息
const loginResult = await accountLoginRequest(account)
const id = loginResult.data.id
this.token = loginResult.data.token
localCache.setCache(LOGIN_TOKEN, this.token)//本地缓存
// 2.获取登录用户的详细信息(role信息)
const userInfoResult = await getUserInfoById(id)
const userInfo = userInfoResult.data
this.userInfo = userInfo
// 3.根据角色请求用户的权限(菜单menus)
const userMenusResult = await getUserMenusByRoleId(this.userInfo.role.id)
const userMenus = userMenusResult.data
this.userMenus = userMenus
// 4.进行本地缓存
localCache.setCache('userInfo', userInfo)
localCache.setCache('userMenus', userMenus)
// 5.请求所有roles/departments数据
// 重要: 获取登录用户的所有按钮的权限
// 重要: 动态的添加路由
// 5.页面跳转(main页面)
router.push('/main')
}
}
})
export default useLoginStore
退出登录
// /views/main/Main.ue
<button @click="handleExitClick">
退出登录
</button>
const router =useRouter()
function handleExitClick(){
//1.删除token
localCache.removeCache(LOGIN_TOKEN)
//2.返回到login页面
router.push{'/login'}
}
localStorage封装
//utils/cache.ts
//枚举类型
enum CacheType {
Local,
Session
}
class Cache {
storage: Storage
constructor(type: CacheType) {
this.storage = type === CacheType.Local ? localStorage : sessionStorage
}
setCache(key: string, value: any) {
if (value) {
this.storage.setItem(key, JSON.stringify(value))
}
}
getCache(key: string) {
const value = this.storage.getItem(key)
if (value) {
return JSON.parse(value)
}
}
removeCache(key: string) {
this.storage.removeItem(key)
}
clear() {
this.storage.clear()
}
}
const localCache = new Cache(CacheType.Local)
const sessionCache = new Cache(CacheType.Session)
export { localCache, sessionCache }
路由导航守卫
// router/index.ts
router.deforeEach((to)=>{
const token=localCache.getCache(LOGIN_TOKEN)
if(to.path.startsWith('/main')&&!token){
return '/login'
}
})
请求头统一注入token
// service/index.ts
import { LOGIN_TOKEN } from '@/global/constants'
import { localCache } from '@/utils/cache'
import { BASE_URL, TIME_OUT } from './config'
import HYRequest from './request'
const hyRequest = new HYRequest({
baseURL: BASE_URL,
timeout: TIME_OUT,
interceptors: {
requestSuccessFn: (config) => {
// 每一个请求都自动携带token
const token = localCache.getCache(LOGIN_TOKEN)
if (config.headers && token) {
// 类型缩小
config.headers.Authorization = 'Bearer ' + token
}
return config
}
}
})
export default hyRequest