编辑 src / views / login / index.vue
文件
<template>
<div class="login-container">
<el-form
class="login-form"
:rules="rules"
ref="form"
:model="user"
size="medium"
@submit.prevent="handleSubmit"
>
<div class="login-form__header">
<img
class="login-logo"
src="@/assets/login_logo.png"
alt="拉勾心选"
>
div>
<el-form-item prop="account">
<el-input
v-model="user.account"
placeholder="请输入用户名"
>
<template #prefix>
<i class="el-input__icon el-icon-user" />
template>
el-input>
el-form-item>
<el-form-item prop="pwd">
<el-input
v-model="user.pwd"
type="password"
placeholder="请输入密码"
>
<template #prefix>
<i class="el-input__icon el-icon-lock" />
template>
el-input>
el-form-item>
<el-form-item prop="imgcode">
<div class="imgcode-wrap">
<el-input
v-model="user.imgcode"
placeholder="请输入验证码"
>
<template #prefix>
<i class="el-input__icon el-icon-key" />
template>
el-input>
<img
class="imgcode"
alt="验证码"
src="https://shop.fed.lagou.com/api/admin/captcha_pro"
>
div>
el-form-item>
<el-form-item>
<el-button
class="submit-button"
type="primary"
:loading="loading"
native-type="submit"
>
登录
el-button>
el-form-item>
el-form>
div>
template>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
const user = reactive({
account: 'admin',
pwd: '123456',
imgcode: ''
})
const loading = ref(false)
const rules = ref({
account: [
{ required: true, message: '请输入账号', trigger: 'change' }
],
pwd: [
{ required: true, message: '请输入密码', trigger: 'change' }
],
imgcode: [
{ required: true, message: '请输入验证码', trigger: 'change' }
]
})
const handleSubmit = async () => {
console.log('handleSubmit')
}
script>
<style lang="scss" scoped>
.login-container {
min-width: 400px;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: #2d3a4b;
}
.login-form {
padding: 30px;
border-radius: 6px;
background: #fff;
min-width: 350px;
.login-form__header {
display: flex;
justify-content: center;
align-items: center;
padding-bottom: 30px;
}
.el-form-item:last-child {
margin-bottom: 0;
}
.login__form-title {
display: flex;
justify-content: center;
color: #fff;
}
.submit-button {
width: 100%;
}
.login-logo {
width: 271px;
height: 74px;
}
.imgcode-wrap {
display: flex;
align-items: center;
.imgcode {
height: 37px;
}
}
}
style>
编辑 src / api / common.ts
文件
...
export const getCaptcha = () => {
return request<Blob>({
method: 'GET',
url: '/admin//captcha_pro',
params: {
stamp: Date.now()
},
responseType: 'blob' // 请求获取图片数据
})
}
<template>
...
<img
class="imgcode"
alt="验证码"
:src="captchaSrc"
@click="loadCaptcha"
>
div>
el-form-item>
...
template>
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue'
import { getCaptcha } from '@/api/common'
...
const captchaSrc = ref('')
onMounted(() => {
loadCaptcha()
})
const loadCaptcha = async () => { // 获取图片验证码
const data = await getCaptcha()
captchaSrc.value = URL.createObjectURL(data)
}
...
script>
编辑 src / utils / request.ts
文件
...
// 接口返回数据处理
export default <T = any>(config: AxiosRequestConfig) => {
return request(config).then(res => {
return (res.data.data || res.data) as T
})
}
编辑 src / api / common.ts
文件
...
export const login = (data: {
account: string,
pwd: string,
imgcode: string
}) => { // 用户登录请求
return request<ILoginResponse>({
method: 'POST',
url: '/admin/login',
data
})
}
编辑 src / api / types / common.ts
文件
// 登录请求
export interface IUserInfo {
id: number
account: string
head_pic: string
}
export interface IMenu {
path: string
title: string
icon: string
header: string
is_header: number
children?: IMenu[]
}
export interface ILoginResponse {
token: string
expires_time: number
menus: IMenu[]
unique_auth: string[]
user_info: IUserInfo
logo: string
logo_square: string
version: string
newOrderAudioLink: string
}
编辑 src / views / login / index.vue
文件
...
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue'
import { getCaptcha, login } from '@/api/common'
import { ElForm } from 'element-plus'
import { useRouter } from 'vue-router'
const router = useRouter()
const user = reactive({
account: 'admin',
pwd: '123456',
imgcode: ''
})
const loading = ref(false)
const rules = ref({
account: [
{ required: true, message: '请输入账号', trigger: 'change' }
],
pwd: [
{ required: true, message: '请输入密码', trigger: 'change' }
],
imgcode: [
{ required: true, message: '请输入验证码', trigger: 'change' }
]
})
const captchaSrc = ref('')
const form = ref<InstanceType<typeof ElForm> | null>(null)
onMounted(() => {
loadCaptcha()
})
const loadCaptcha = async () => { // 获取图片验证码
const data = await getCaptcha()
captchaSrc.value = URL.createObjectURL(data)
}
const handleSubmit = async () => { // 登录请求
// 表单验证
const valid = await form.value?.validate()
if (!valid) {
return false
}
// 验证通过, 展示loading
loading.value = true
// 请求提交
const data = await login(user).finally(() => {
loading.value = false
})
console.log('data=>', data)
router.replace({
name: 'home'
})
}
script>
...
编辑 src / utils / request.ts
文件
// 响应拦截器
request.interceptors.response.use(function (response) {
// 统一设置接口相应错误, 比如 token 过期失效, 服务端异常
if (response.data.status && response.data.status !== 200) { // 后端返回访问失败
ElMessage.error(response.data.msg || '请求失败, 请刷新后重试')
// 手动返回一个 Promise 异常
return Promise.reject(response.data)
}
return response
}, function (error) {
// Do something with response error
return Promise.reject(error)
})
新建 src / types / element-plus.ts
文件
import { ElForm } from 'element-plus'
import type { FormItemRule } from 'element-plus/es/components/form/src/form.type'
export type IElForm = InstanceType<typeof ElForm>
export type IFormRule = Record<string, FormItemRule[]>
编辑 src / views / login / index.vue
文件
...
import type { IElForm, IFormRule } from '@/types/element-plus'
// const rules = ref({
const rules = ref<IFormRule>({
// const form = ref | null>(null)
const form = ref<IElForm | null>(null)
...
编辑 src / views / login / index.vue
文件
<script lang="ts" setup>
...
// 请求提交
const data = await login(user).finally(() => {
loading.value = false
})
store.commit('setUser', data.user_info)
console.log('data =>', data)
...
script>
编辑 src / store / index.vue
文件
import { IUserInfo } from '@/api/types/common'
const state = {
count: 1,
isCollapse: false,
user: JSON.parse(window.localStorage.getItem('user') || 'null') as IUserInfo | null
}
...
mutations: {
...
setUser (state, payload) {
state.user = payload
// 本地存储
window.localStorage.setItem('user', JSON.stringify(state.user))
...
新建 src / layout / components / AppHeader / components / UserInfo.vue
文件
<template>
<el-dropdown>
<span class="el-dropdown-link">
{{ $store.state.user?.account }}
<el-icon class="el-icon--right">
<arrow-down />
el-icon>
span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>个人中心el-dropdown-item>
<el-dropdown-item>退出登录el-dropdown-item>
el-dropdown-menu>
template>
el-dropdown>
template>
<script lang="ts" setup>
import { ArrowDown } from '@element-plus/icons-vue'
script>
<style lang="scss" scoped>style>
编辑 src / layout / components / AppHeader / index.vue
文件
<template>
<el-space>
<ToggleSidebar />
<BreadcrumbVue />
el-space>
<el-space>
<FullScreen />
<UserInfoVue />
el-space>
template>
<script lang="ts" setup>
import ToggleSidebar from './components/ToggleSidebar.vue'
import BreadcrumbVue from './components/Breadcrumb.vue'
import UserInfoVue from './components/UserInfo.vue'
import FullScreen from './components/FullScreen.vue'
script>
<style lang="scss" scoped>style>
新建 src / utils / storage.ts
文件
export const getItem = <T>(key: string) => {
const data = window.localStorage.getItem(key)
if (!data) return null
try {
return JSON.parse(data) as T
} catch (err) {
return null
}
}
export const setItem = (key: string, value: object | string | null) => {
if (typeof value === 'object') {
value = JSON.stringify(value)
}
window.localStorage.setItem(key, value)
}
export const removeItem = (key: string) => {
window.localStorage.removeItem(key)
}
编辑 src / store / index.ts
文件
import { getItem, setItem } from '@/utils/storage'
// user: JSON.parse(window.localStorage.getItem('user') || 'null') as IUserInfo | null
user: getItem<IUserInfo>('user')
// window.localStorage.setItem('user', JSON.stringify(state.user))
setItem('user', state.user)
新建 src / utils / constants.ts
文件
export const USER = 'USER'
编辑 src / store / index.ts
文件
import { USER } from '@/utils/constants'
// user: getItem('user')
user: getItem<IUserInfo>(USER)
// setItem('user', state.user)
setItem(USER, state.user)
编辑 src / api / common.ts
文件
...
export const logout = () => { // 管理员退出
return request<ILoginResponse>({
method: 'GET',
url: '/setting/admin/logout'
})
}
编辑 src / layout / components / AppHeader / components / UserInfo.vue
文件
<template>
<el-dropdown>
<span class="el-dropdown-link">
{{ $store.state.user?.account }}
<el-icon class="el-icon--right">
<arrow-down />
el-icon>
span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>个人中心el-dropdown-item>
<el-dropdown-item @click="handleLogout">
退出登录
el-dropdown-item>
el-dropdown-menu>
template>
el-dropdown>
template>
<script lang="ts" setup>
import { logout } from '@/api/common'
import { ArrowDown } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { store } from '@/store'
const router = useRouter()
const handleLogout = () => {
// 确认提示
ElMessageBox.confirm(
'是否确认退出?',
'提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(async () => {
// 退出请求
await logout()
// 跳转登录页
router.push({
name: 'login'
})
// 清除用户信息
store.commit('setUser', null)
ElMessage({
type: 'success',
message: '退出成功'
})
})
.catch(() => {
ElMessage({
type: 'info',
message: '已取消退出'
})
})
}
script>
<style lang="scss" scoped>style>
编辑 src / views / login / index.vue
文件
const handleSubmit = async () => { // 登录请求
...
store.commit('setUser', {
...data.user_info,
token: data.token
})
...
编辑 src / utils / request.ts
文件
// 根据不同环境 切换不同路径
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASEURL
})
// 请求拦截器
request.interceptors.request.use(function (config: any) {
// 统一设置用户身份 token
const user = store.state.user
if (user && user.token) {
config.headers.Authorization = `Bearer ${user.token}`
}
return config
}, function (error) {
return Promise.reject(error)
})
这里退出并没有成功, 因为退出需要传递用户 token 作为标识
路由元信息
编辑 router.d.ts
文件
import 'vue-router'
declare module 'vue-router' {
// eslint-disable-next-line no-unused-vars
interface RouteMeta {
title?: string,
requiresAuth?: boolean
}
}
编辑 src / router / index.ts
文件
...
path: '', // 默认子路由
name: 'home',
component: () => import('../views/home/index.vue'),
meta: { title: '首页', requiresAuth: true }
},
...
// 全局前置守卫
router.beforeEach((to, form) => {
nprogress.start() // 开始加载进度条
if (to.meta.requiresAuth && !store.state.user) {
// 此路由需要授权,请检查是否已登录
// 如果没有,则重定向到登录页面
return {
path: '/login',
// 保存我们所在的位置,以便以后再来
query: { redirect: to.fullPath }
}
}
})
...
编辑 src / router / modules /product.ts
文件
...
path: 'product',
component: RouterView,
meta: {
title: '商品',
requiresAuth: true
},
...
编辑 src / views / login / index.vue
文件
...
console.log('data =>', data)
// 获取当前路由对象
let redirect = route.query.redirect || '/'
if (typeof redirect !== 'string') {
redirect = '/'
}
router.replace(redirect)
...
编辑 src / utils / request.ts
文件
import axios, { AxiosRequestConfig } from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { store } from '@/store'
import router from '@/router/'
// 根据不同环境 切换不同路径
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASEURL
})
// 请求拦截器
request.interceptors.request.use(function (config: any) {
// 统一设置用户身份 token
const user = store.state.user
if (user && user.token) {
config.headers.Authorization = `Bearer ${user.token}`
}
return config
}, function (error) {
return Promise.reject(error)
})
// 控制登录过期的锁
let isRefreshing = false
// 响应拦截器
request.interceptors.response.use(function (response) {
// 统一设置接口相应错误, 比如 token 过期失效, 服务端异常
// if (response.data.status && response.data.status !== 200) { // 后端返回访问失败
// ElMessage.error(response.data.msg || '请求失败, 请刷新后重试')
// // 手动返回一个 Promise 异常
// return Promise.reject(response.data)
// }
// return response
const status = response.data.status
if (!status || status === 200) { // 正常情况
return response
}
if (status === 410000) { // 异常情况: token 过期...
if (isRefreshing) return Promise.reject(response)
isRefreshing = true
// 提示是否跳转登录页
ElMessageBox.confirm('你的登录状态已经过期, 是否前往登录页面?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消'
})
.then(() => {
// 清除本地过期登录状态
store.commit('setUser', null)
// 跳转登录页面
router.push({
name: 'login',
query: {
redirect: router.currentRoute.value.fullPath
}
})
// 抛出异常
})
.finally(() => {
isRefreshing = false
})
// 内部这个消化业务异常
return Promise.reject(response)
}
// 其他情况
ElMessage.error(response.data.msg || '请求失败, 请刷新后重试')
// 手动返回一个 Promise 异常
return Promise.reject(response.data)
}, function (error) {
return Promise.reject(error)
})
// 接口返回数据处理
export default <T = any>(config: AxiosRequestConfig) => {
return request(config).then(res => {
return (res.data.data || res.data) as T
})
}