中文官网
https://element-plus.org/zh-CN/
第一步:安装 Element-plus
npm install element-plus --save
第二步: 安装 首先你需要安装unplugin-vue-components
和 unplugin-auto-import
这两款插件
npm install -D unplugin-vue-components unplugin-auto-import
第三步:打开vite.config.ts 配置两款插件
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@router': fileURLToPath(new URL('./src/router', import.meta.url)),
'@stores': fileURLToPath(new URL('./src/stores', import.meta.url)),
'@views': fileURLToPath(new URL('./src/views', import.meta.url)),
'@components': fileURLToPath(new URL('./src/components', import.meta.url)),
},
},
})
安装 字体图片
npm install @element-plus/icons-vue
第四步,在 main.ts 中引入Element-plus
// 重置 css 样式
import '@css/custom.init.css'
import '@css/init.css'
// import './assets/main.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
// 添加字体图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia())
app.use(router)
app.mount('#app')
icon图标
官网:https://element-plus.org/zh-CN/component/icon.html
第1步:安装 @element-plus/icons-vue
npm install @element-plus/icons-vue
yarn add @element-plus/icons-vue
pnpm install @element-plus/icons-vue
第2步:在 main.ts 需要从 @element-plus/icons-vue 中导入所有图标并进行全局注册
import './assets/main.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
// 添加字体图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia())
app.use(router)
app.mount('#app')
第3步:复制图标代码
<el-icon><UserFilled /></el-icon>
登录页布局 实现代码
<template>
<div class="login">
<el-form class="login-form">
<el-form-item>
<h2 class="login-title">隆迟电商基础框架</h2>
</el-form-item>
<el-form-item>
<el-input size="large" placeholder="用户名">
<template #prefix>
<el-icon><User /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-input size="large" placeholder="密码" show-password>
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<div class="form-code">
<el-input class="code-input" size="large" placeholder="验证码" :maxlength="4" show-word-limit>
<template #prefix>
<el-icon><Aim /></el-icon>
</template>
</el-input>
<el-image class="code-image"></el-image>
</div>
</el-form-item>
<el-form-item>
<el-checkbox>记住密码</el-checkbox>
</el-form-item>
<el-form-item>
<el-button size="large" type="primary" class="form-submit">登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<style scoped>
.login {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
inset: 0;
background: url('../assets/images/login_background.png') no-repeat center top;
/* background: url('https://luxian-ai.oss-cn-beijing.aliyuncs.com/luxian-ai/avatar/2024-07-28/1722173053591686.jpg') no-repeat center top; */
background-size: cover;
overflow: hidden;
}
.login-form {
padding: 3rem 2rem;
width: 400px;
background-color: #ffffff;
}
.login-title {
width: 100%;
text-align: center;
font-weight: bold;
font-size: 22px;
}
.form-code {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
width: 100%;
}
.code-input {
flex: 1;
}
.code-image {
width: 100px;
height: 40px;
cursor: pointer;
}
.form-submit {
width: 100%;
}
</style>
以上代码实现如下页面

axios二次封装
我们在每一个页面都要去判断登录的 Token 是否过期,即 res.code--401的时候就过期了,我们判断一个还好,那10个20个这样的状态码,这样就太累了,所以关于后端返回的数据,
.then(res=>{
console.log(res);
})
以上内容能否封装起来,统一由某一个文件去做判断
以下是axios二次要封装的内容
axios({
url:'/captcha/image',
method:'get',
responseType:'arraybuffer',
params:{
key:'123456'
}.then(res => {
console.log(res);
})
})
前端请求数据封装的内容如下:
url:'/captcha/image',
method:'get',
responseType:'arraybuffer',
params:{
key:'123456'
}
后端返回数据要封装的内容:
.then(res => {
console.log(res);
})
我们每个页面只需要发送请求就可以了。我们将后端返回的数据单独交给某一个文件去处理。这就是我们封装的意义所在。
第二个我们封装是我们的请求,由前端给后端发送的数据封装到某一个文件中,比如说几个页面的请求都需要前端将token传递给后端,我们不能每个页面去写,
header:{
token:'12345678'
}
可能统一的,每一个请求都可能涉及,或者说有一个变量,每个页面都要用,他可能改变了,他改变了所有都跟着改变,或者统一要进行判断,我们给某一个文件就可以了,不需要在每一个里面去做,要不然这样太累了。
所以axios封装是为了好管理,好维护,统一判断,统一传值,其实就是让我们的管理性和维护性更高,这就是封装的意义。
具体要封装上面呢?
封装的内容:
1,前端请求后端接口的时候,有一些接口需要传递 Token (统一在某一个文件中做旧可以),不需要每个请求单独再写
2,前端请求后端接口的时候,后端返回数据,其中有状态码,我们可能要根据返回的状态码来判断(也是统一要在某一个文件中处理,避免每一个文件在请求时再单独写一份)
简单封装如下,以后有需求可以继续往里面添加内容 src/utils/request
import axios from 'axios';
import { ElMessage } from 'element-plus';
// axios.get('/api/data');
// axios.post('/api/data');
// 1. 创建 axios 对象
const request = axios.create({
baseURL: '/api', // 你的后端接口地址
timeout: 5000 // 请求超时时间
});
// 2. 添加请求拦截器(前端请求后端,前端给后端传数据时,可以设置一些拦截器,比如token之类的信息)
request.interceptors.request.use(function(config) {
return config;
})
// 3. 添加响应拦截器(后端给前端返回数据,前端接收数据时,可以设置一些拦截器,比如token之类的信息)
request.interceptors.response.use(function(response) {
if (response.data instanceof ArrayBuffer) {
return response.data;
}
let { code, msg, data } = response.data;
if (code == 200) {
return data;
}
if (msg) {
ElMessage({type: 'error',message: msg})
}
// 捕获异常信息
throw msg;
})
// 4. 封装请求方法 (封装成一个对象,方便调用) get post
const http = {
get(url, params, config) {
return new Promise((resolve, reject) => {
request.get(url,{params,...config})
.then(res => {
resolve(res)
})
.catch(error => {
reject(error)
})
})
},
post(url, data, config) {
return new Promise((resolve, reject) => {
request.post(url,{data,config})
.then(res => {
resolve(res)
})
.catch(error => {
reject(error)
})
})
}
}
export default http;
所有请求文件 src/api/login.ts
// 前端请求后端的所有请求都放在这个文件里统一管理
import http from '@utils/request'
// 图形验证码 获取验证码图片接口
interface ICaptchaImage {
key: string
}
export const captchaImage = (data: ICaptchaImage) => {
return http.get('/captcha/imageCode', data, {responseType:'arraybuffer'})
}
// 用户登录接口
export interface RuleForm {
username: string
password: string
key: string
captcha: string
}
export const loginByJson = (data: RuleForm) => {
return http.post('/u/loginByJson', data)
}
// 个人信息
export const getInfo = () => {
return http.get('/personal/getInfo')
}
// 获取路由信息
export const getRouters = (data) => {
return http.get('/personal/getRouters/${data}')
}
使用场景部分代码 src/views/login/Login.vue
<script setup lang="ts">
import { reactive, ref, onBeforeMount } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { captchaImage, RuleForm, loginByJson } from '@api/login';
import { Encrypt } from '@utils/aes';
import { useRouter } from 'vue-router';
import { useUserStore } from '@stores/userStore';
const router = useRouter();
const ruleForm = reactive<RuleForm>({
username: '',
password: '',
key: '',
captcha: '',
})
const rules = reactive<FormRules<RuleForm>>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 5, max: 10, message: '位数为5-10位', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 5, max: 10, message: '位数为5-10位', trigger: 'blur' },
],
captcha: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
],
})
const captcha = reactive({
url: ''
})
const getCodeImg = async () => {
// 获取时间戳作为key,防止缓存问题
const key = new Date().getTime().toString();
ruleForm.key = key;
let res = await captchaImage({ key });
let blob = new Blob([res], { type: 'application/vnd.ms-excel' });
let imgUrl = URL.createObjectURL(blob);
captcha.url = imgUrl;
}
const ruleFormRef = ref<FormInstance>()
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async(valid, fields) => {
if (valid) {
const store = useUserStore();
let res = await loginByJson({
username:Encrypt(ruleForm.username),
password:Encrypt(ruleForm.password),
key:ruleForm.key,
captcha:ruleForm.captcha,
});
// 存储token到本地存储中
localStorage.setItem('token', res);
await store.initUserInfoAndConfig();
// 跳转到首页
// router.push('/');
} else {
console.log('error submit!', fields)
}
})
}
onBeforeMount(() => {
getCodeImg();
})
</script>
使用场景完整代码 src/views/login/Login.vue
<template>
<div class="login">
<el-form class="login-form" :module="ruleForm" :rules="rules" ref="ruleFormRef">
<el-form-item>
<h2 class="login-title">隆迟电商基础框架</h2>
</el-form-item>
<el-form-item label="用户名:" prop="username">
<el-input size="large" placeholder="用户名" v-model="ruleForm.username">
<template #prefix>
<el-icon><User /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="密码:" prop="password">
<el-input size="large" placeholder="密码" show-password v-model="ruleForm.password">
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="验证码:" prop="captcha">
<div class="form-code">
<el-input class="code-input" size="large" placeholder="验证码" :maxlength="4" show-word-limit v-model="ruleForm.captcha">
<template #prefix>
<el-icon><Aim /></el-icon>
</template>
</el-input>
<el-image class="code-image" :src="captcha.url" @click="getCodeImg"></el-image>
</div>
</el-form-item>
<el-form-item>
<el-checkbox>记住密码</el-checkbox>
</el-form-item>
<el-form-item>
<el-button size="large" type="primary" class="form-submit" @click="submitForm(ruleFormRef)">登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onBeforeMount } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { captchaImage, RuleForm, loginByJson } from '@api/login';
import { Encrypt } from '@utils/aes';
import { useRouter } from 'vue-router';
import { useUserStore } from '@stores/userStore';
const router = useRouter();
const ruleForm = reactive<RuleForm>({
username: '',
password: '',
key: '',
captcha: '',
})
const rules = reactive<FormRules<RuleForm>>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 5, max: 10, message: '位数为5-10位', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 5, max: 10, message: '位数为5-10位', trigger: 'blur' },
],
captcha: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
],
})
const captcha = reactive({
url: ''
})
const getCodeImg = async () => {
// 获取时间戳作为key,防止缓存问题
const key = new Date().getTime().toString();
ruleForm.key = key;
let res = await captchaImage({ key });
let blob = new Blob([res], { type: 'application/vnd.ms-excel' });
let imgUrl = URL.createObjectURL(blob);
captcha.url = imgUrl;
}
const ruleFormRef = ref<FormInstance>()
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async(valid, fields) => {
if (valid) {
const store = useUserStore();
let res = await loginByJson({
username:Encrypt(ruleForm.username),
password:Encrypt(ruleForm.password),
key:ruleForm.key,
captcha:ruleForm.captcha,
});
// 存储token到本地存储中
localStorage.setItem('token', res);
await store.initUserInfoAndConfig();
// 跳转到首页
// router.push('/');
} else {
console.log('error submit!', fields)
}
})
}
onBeforeMount(() => {
getCodeImg();
})
</script>
<style scoped>
.login {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
inset: 0;
background: url('../assets/images/login_background.png') no-repeat center top;
/* background: url('https://luxian-ai.oss-cn-beijing.aliyuncs.com/luxian-ai/avatar/2024-07-28/1722173053591686.jpg') no-repeat center top; */
background-size: cover;
overflow: hidden;
}
.login-form {
padding: 3rem 2rem;
width: 400px;
background-color: #ffffff;
}
.login-title {
width: 100%;
text-align: center;
font-weight: bold;
font-size: 22px;
}
.form-code {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
width: 100%;
}
.code-input {
flex: 1;
}
.code-image {
width: 100px;
height: 40px;
cursor: pointer;
}
.form-submit {
width: 100%;
}
</style>
跨域问题

配置代理
配置代理 目的:解决开发阶段跨域问题 要么前端解决,要么后端解决
解决办法: 打开 vite.config.ts
配置代理 解决跨域问题
在 vite.config.ts 文件中添加如下代码解决跨域问题
server: {
proxy: {
'/api': {
target:'http://uat.admin.banlu.xuexiluxian.cn',
changeOrigin: true,
rewrite: path=>path.replace(/^\/api/,'')
}
}
}
配置代理 完整代码
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@router': fileURLToPath(new URL('./src/router', import.meta.url)),
'@stores': fileURLToPath(new URL('./src/stores', import.meta.url)),
'@views': fileURLToPath(new URL('./src/views', import.meta.url)),
'@components': fileURLToPath(new URL('./src/components', import.meta.url)),
'@images': fileURLToPath(new URL('./src/assets/images', import.meta.url)),
'@css': fileURLToPath(new URL('./src/assets/css', import.meta.url)),
'@utils': fileURLToPath(new URL('./src/utils', import.meta.url)),
'@api': fileURLToPath(new URL('./src/api', import.meta.url)),
'@layout': fileURLToPath(new URL('./src/layout', import.meta.url)),
},
},
// 配置代理 解决跨域问题
server:{
proxy:{
'/api':{
target:'http://uat.admin.banlu.xuexiluxian.cn/api',
changeOrigin:true,
rewrite:path=>path.replace(/^\/api/,'')
}
}
}
})
执行代码,截屏如下 跨域问题完美解决

api解耦
封装内容:
import http from '@utils/request'
// 图形验证码 获取验证码图片接口
interface ICaptchaImage {
key: string
}
export const captchaImage = (data: ICaptchaImage) => {
return http.get('/captcha/imageCode', data, {responseType:'arraybuffer'})
}
使用示例如下: api解耦二次封装的内容
<script setup>
import { captchaImage } from '@api/login'
captchaImage({key:'123456'}).then(res=>{
console.log(res);
})
</script>
登录页数据和验证码渲染实现代码如下
1, src/views/HomeView.vue
<template>
<div class="login">
<el-form class="login-form" :module="ruleForm" :rules="rules">
<el-form-item>
<h2 class="login-title">隆迟电商基础框架</h2>
</el-form-item>
<el-form-item prop="username">
<el-input size="large" placeholder="用户名" v-model="ruleForm.username">
<template #prefix>
<el-icon><User /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input size="large" placeholder="密码" show-password v-model="ruleForm.password">
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="captcha">
<div class="form-code">
<el-input class="code-input" size="large" placeholder="验证码" :maxlength="4" show-word-limit v-model="ruleForm.captcha">
<template #prefix>
<el-icon><Aim /></el-icon>
</template>
</el-input>
<el-image class="code-image" :src="captcha.url" @click=""></el-image>
</div>
</el-form-item>
<el-form-item>
<el-checkbox>记住密码</el-checkbox>
</el-form-item>
<el-form-item>
<el-button size="large" type="primary" class="form-submit">登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { reactive, onBeforeMount } from 'vue'
import type { FormRules } from 'element-plus'
import { captchaImage, RuleForm } from '@api/login';
const ruleForm = reactive<RuleForm>({
username: '',
password: '',
key: '',
captcha: '',
})
const rules = reactive<FormRules<RuleForm>>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 5, max: 10, message: '用户名为5-10位', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 5, max: 10, message: '密码为5-10位', trigger: 'blur' },
],
captcha: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
],
})
const captcha = reactive({
url: ''
})
const getCodeImg = async () => {
// 获取时间戳作为key,防止缓存问题
const key = new Date().getTime().toString();
ruleForm.key = key;
let res = await captchaImage({ key });
let blob = new Blob([res], { type: 'application/vnd.ms-excel' });
let imgUrl = URL.createObjectURL(blob);
captcha.url = imgUrl;
}
onBeforeMount(() => {
getCodeImg();
})
</script>
<style scoped>
.login {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
inset: 0;
background: url('../assets/images/login_background.png') no-repeat center top;
/* background: url('https://luxian-ai.oss-cn-beijing.aliyuncs.com/luxian-ai/avatar/2024-07-28/1722173053591686.jpg') no-repeat center top; */
background-size: cover;
overflow: hidden;
}
.login-form {
padding: 3rem 2rem;
width: 400px;
background-color: #ffffff;
}
.login-title {
width: 100%;
text-align: center;
font-weight: bold;
font-size: 22px;
}
.form-code {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
width: 100%;
}
.code-input {
flex: 1;
}
.code-image {
width: 100px;
height: 40px;
cursor: pointer;
}
.form-submit {
width: 100%;
}
</style>
2, src/api/login.ts
import http from '@utils/request'
// 图形验证码 获取验证码图片接口
interface ICaptchaImage {
key: string
}
export const captchaImage = (data: ICaptchaImage) => {
return http.get('/captcha/imageCode', data, {responseType:'arraybuffer'})
}
// 图形验证码 验证接口
export interface RuleForm {
username: string
password: string
key: string
captcha: string
}
3, src/utils/request.ts
import axios from 'axios';
// 1. 创建 axios 对象
const request = axios.create({
baseURL: '/api', // 你的后端接口地址
// timeout: 5000 // 请求超时时间
});
// 2. 添加请求拦截器(前端请求后端,前端给后端传数据时,可以设置一些拦截器,比如token之类的信息)
request.interceptors.request.use(function(config) {
return config;
})
// 3. 添加响应拦截器(后端给前端返回数据,前端接收数据时,可以设置一些拦截器,比如token之类的信息)
request.interceptors.response.use(function(response) {
return response;
})
// 4. 封装请求方法 (封装成一个对象,方便调用) get post
const http = {
get(url, params, config) {
return new Promise((resolve, reject) => {
request.get(url,{params, ...config})
.then(res => resolve(res.data))
.catch(error => reject(error));
})
},
post(url, data, config) {
return new Promise((resolve, reject) => {
request.post(url,{data,config})
.then(res => resolve(res.data))
.catch(error => reject(error));
})
}
}
export default http;
安装 crypto-js
npm install crypto-js
npm install --save-dev @types/crypto-js
src/utils/aes.ts
// 引入AES源码js
import CryptoJS from 'crypto-js'
// 默认的KEY与IV如果没有给
// 秘钥: bGvnMc62sh5RV6zP
// 偏移量: 1eZ43DLcYtV2xb3Y
const key = CryptoJS.enc.Utf8.parse('bGvnMc62sh5RV6zP')
// 十六位十六进制数作为秘钥
const iv = CryptoJS.enc.Utf8.parse('1eZ43DLcYtV2xb3Y')
// 十六位十六进制数作为秘钥偏移量
// 解密方法
export function Decrypto(word) {
const encryptedHexStr = CryptoJS.enc.Hex.parse(word)
const srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr)
const decrypt = CryptoJS.AES.decrypt(srcs, key, {iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7})
const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8)
return decryptedStr.toString()
}
// 加密方法
export function Encrypt(word) {
const srcs = CryptoJS.enc.Utf8.parse(word)
const encrypted = CryptoJS.AES.encrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 })
return encrypted.ciphertext.toString().toUpperCase()
}
登录的内容 src/views/HomeView.vue
<template>
<div class="login">
<el-form class="login-form" :module="ruleForm" :rules="rules" ref="ruleFormRef">
<el-form-item>
<h2 class="login-title">隆迟电商基础框架</h2>
</el-form-item>
<el-form-item prop="username">
<el-input size="large" placeholder="用户名" v-model="ruleForm.username">
<template #prefix>
<el-icon><User /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input size="large" placeholder="密码" show-password v-model="ruleForm.password">
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="captcha">
<div class="form-code">
<el-input class="code-input" size="large" placeholder="验证码" :maxlength="4" show-word-limit v-model="ruleForm.captcha">
<template #prefix>
<el-icon><Aim /></el-icon>
</template>
</el-input>
<el-image class="code-image" :src="captcha.url" @click="getCodeImg"></el-image>
</div>
</el-form-item>
<el-form-item>
<el-checkbox>记住密码</el-checkbox>
</el-form-item>
<el-form-item>
<el-button size="large" type="primary" class="form-submit" @click="submitForm(ruleFormRef)">登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onBeforeMount } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { captchaImage, RuleForm, loginByJson } from '@api/login';
import { Encrypt } from '@utils/aes';
import type { User } from '@element-plus/icons-vue';
const ruleForm = reactive<RuleForm>({
username: '',
password: '',
key: '',
captcha: '',
})
const rules = reactive<FormRules<RuleForm>>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 5, max: 10, message: '位数为5-10位', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 5, max: 10, message: '位数为5-10位', trigger: 'blur' },
],
captcha: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
],
})
const captcha = reactive({
url: ''
})
const getCodeImg = async () => {
// 获取时间戳作为key,防止缓存问题
const key = new Date().getTime().toString();
ruleForm.key = key;
let res = await captchaImage({ key });
let blob = new Blob([res], { type: 'application/vnd.ms-excel' });
let imgUrl = URL.createObjectURL(blob);
captcha.url = imgUrl;
}
const ruleFormRef = ref<FormInstance>()
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async(valid, fields) => {
if (valid) {
let res = await loginByJson({
username:Encrypt(ruleForm.username),
password:Encrypt(ruleForm.password),
key:ruleForm.key,
captcha:ruleForm.captcha,
});
if(res.code != 200) console.log('输入内容有误,请重新输入!');
console.log(res.data);
} else {
console.log('error submit!', fields)
}
})
}
onBeforeMount(() => {
getCodeImg();
})
</script>
<style scoped>
.login {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
inset: 0;
background: url('../assets/images/login_background.png') no-repeat center top;
/* background: url('https://luxian-ai.oss-cn-beijing.aliyuncs.com/luxian-ai/avatar/2024-07-28/1722173053591686.jpg') no-repeat center top; */
background-size: cover;
overflow: hidden;
}
.login-form {
padding: 3rem 2rem;
width: 400px;
background-color: #ffffff;
}
.login-title {
width: 100%;
text-align: center;
font-weight: bold;
font-size: 22px;
}
.form-code {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
width: 100%;
}
.code-input {
flex: 1;
}
.code-image {
width: 100px;
height: 40px;
cursor: pointer;
}
.form-submit {
width: 100%;
}
</style>
登录后获取当前登录用户路由
获取左侧菜单流程
1, 登录成功,获取到token
2, 请求个人信息接口,把token传递过去,获取个人信息==>角色权限编码(关键)
3, 请求获取路由接口,把角色权限编码传递过去==>对应这个用户拥有角色菜单(路由)
登录后获取当前登录用户路由 实现代码如下:
1, src/api/login.ts
// 前端请求后端的所有请求都放在这个文件里统一管理
import http from '@utils/request'
// 图形验证码 获取验证码图片接口
interface ICaptchaImage {
key: string
}
export const captchaImage = (data: ICaptchaImage) => {
return http.get('/captcha/imageCode', data, {responseType:'arraybuffer'})
}
// 用户登录接口
export interface RuleForm {
username: string
password: string
key: string
captcha: string
}
export const loginByJson = (data: RuleForm) => {
return http.post('/u/loginByJson', data)
}
// 个人信息
export const getInfo = () => {
return http.get('/personal/getInfo')
}
// 获取路由信息
export const getRouters = (data) => {
return http.get('/personal/getRouters/${data}')
}
2, src/router/guards.ts
export const beforeEach = () => {
console.log('前置111');
};
export const afterEach = () => {
console.log('后置222');
};
3, src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
// 导航守卫
import { beforeEach, afterEach } from './guards'
// 路由表 路由配置文件
import { AppRoutes } from './routes'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: AppRoutes,
})
router.beforeEach(beforeEach)
router.afterEach(afterEach)
export default router
4, src/router/routes.ts
import { markRaw } from "vue";
export const AppRoutes = markRaw([
{
path: '/login',
name: 'login',
component: () => import('@views/login/Login.vue'),
},
{
path: '/',
name: 'home',
component: () => import('@views/home/Home.vue'),
},
])
5, src/stores/menuStore.ts
import { defineStore } from 'pinia'
import { getRouters } from '@api/login'
import { useUserStore } from '@stores/userStore'
export const useMenuStore = defineStore('menu-store',{
state: () => ({
// 这里定义你的状态
}),
actions: {
// 获取用户信息
async loadAuthRouters() {
// console.log('获取路由信息');
const userStore = useUserStore();
const res = await getRouters(userStore.currentRolePerm);
// console.log(userStore.currentRolePerm);
console.log(res);
}
}
})
6, src/stores/userStores.ts
import { defineStore } from 'pinia'
import { getInfo } from '@api/login'
import { useMenuStore } from '@stores/menuStore';
export const useUserStore = defineStore('user-store',{
state: () => ({
// 这里定义你的状态
userInfo: null,
permissions: null,
roles: [],
units: null,
currentRolePerm: sessionStorage.getItem('currentRolePerm') ?? "",
}),
actions: {
// 获取用户信息
async initUserInfoAndConfig() {
// 请求获取个人信息的接口
if (!this.userInfo) {
let {permissions,roles,units,userInfo} = await getInfo();
this.userInfo = userInfo;
this.permissions = permissions;
this.roles = roles;
this.units = units;
if (!this.currentRolePerm) {
this.toggleCurrentRolePerm(roles[0].rolePerm);
}
}
// 获取该用户的路由
await useMenuStore().loadAuthRouters();
},
toggleCurrentRolePerm(rolePerm) {
this.currentRolePerm = rolePerm;
sessionStorage.setItem('currentRolePerm',rolePerm);
}
}
})
7, src/utils/aes.ts
// 引入AES源码js
import CryptoJS from 'crypto-js'
// 默认的KEY与IV如果没有给
// 秘钥: bGvnMc62sh5RV6zP
// 偏移量: 1eZ43DLcYtV2xb3Y
const key = CryptoJS.enc.Utf8.parse('bGvnMc62sh5RV6zP')
// 十六位十六进制数作为秘钥
const iv = CryptoJS.enc.Utf8.parse('1eZ43DLcYtV2xb3Y')
// 十六位十六进制数作为秘钥偏移量
// 解密方法
export function Decrypto(word) {
const encryptedHexStr = CryptoJS.enc.Hex.parse(word)
const srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr)
const decrypt = CryptoJS.AES.decrypt(srcs, key, {iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7})
const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8)
return decryptedStr.toString()
}
// 加密方法
export function Encrypt(word) {
const srcs = CryptoJS.enc.Utf8.parse(word)
const encrypted = CryptoJS.AES.encrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 })
return encrypted.ciphertext.toString().toUpperCase()
}
8, src/utils/request.ts
import axios from 'axios';
import { ElMessage } from 'element-plus';
// axios.get('/api/data');
// axios.post('/api/data');
// 1. 创建 axios 对象
const request = axios.create({
baseURL: '/api', // 你的后端接口地址
timeout: 5000 // 请求超时时间
});
// 2. 添加请求拦截器(前端请求后端,前端给后端传数据时,可以设置一些拦截器,比如token之类的信息)
request.interceptors.request.use(function(config) {
return config;
})
// 3. 添加响应拦截器(后端给前端返回数据,前端接收数据时,可以设置一些拦截器,比如token之类的信息)
request.interceptors.response.use(function(response) {
if (response.data instanceof ArrayBuffer) {
return response.data;
}
let { code, msg, data } = response.data;
if (code == 200) {
return data;
}
if (msg) {
ElMessage({type: 'error',message: msg})
}
// 捕获异常信息
throw msg;
})
// 4. 封装请求方法 (封装成一个对象,方便调用) get post
const http = {
get(url, params, config) {
return new Promise((resolve, reject) => {
request.get(url,{params,...config})
.then(res => {
resolve(res)
})
.catch(error => {
reject(error)
})
})
},
post(url, data, config) {
return new Promise((resolve, reject) => {
request.post(url,{data,config})
.then(res => {
resolve(res)
})
.catch(error => {
reject(error)
})
})
}
}
export default http;
9, src/views/home/Home.vue
<script setup lang="ts">
/*! @file
*******************************************************
<PRE>
文件实现功能 :
作 者 : mary
版本 : 1.0
-------------------------------------------------------
备注 : -
-------------------------------------------------------
修改记录 :
日期 版本 修改人 修改内容
2025/6/18 1.0 mary 创建
</PRE>
******************************************************/
defineOptions({name: ''})
//====================================================
// == 类型定义
//====================================================
// == 初始化
//====================================================
//== 事件处理
</script>
<template>
<div>123</div>
</template>
<style scoped>
</style>
10, src/views/login/Login.vue
<template>
<div class="login">
<el-form class="login-form" :module="ruleForm" :rules="rules" ref="ruleFormRef">
<el-form-item>
<h2 class="login-title">隆迟电商基础框架</h2>
</el-form-item>
<el-form-item label="用户名:" prop="username">
<el-input size="large" placeholder="用户名" v-model="ruleForm.username">
<template #prefix>
<el-icon><User /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="密码:" prop="password">
<el-input size="large" placeholder="密码" show-password v-model="ruleForm.password">
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="验证码:" prop="captcha">
<div class="form-code">
<el-input class="code-input" size="large" placeholder="验证码" :maxlength="4" show-word-limit v-model="ruleForm.captcha">
<template #prefix>
<el-icon><Aim /></el-icon>
</template>
</el-input>
<el-image class="code-image" :src="captcha.url" @click="getCodeImg"></el-image>
</div>
</el-form-item>
<el-form-item>
<el-checkbox>记住密码</el-checkbox>
</el-form-item>
<el-form-item>
<el-button size="large" type="primary" class="form-submit" @click="submitForm(ruleFormRef)">登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onBeforeMount } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { captchaImage, RuleForm, loginByJson } from '@api/login';
import { Encrypt } from '@utils/aes';
import { useRouter } from 'vue-router';
import { useUserStore } from '@stores/userStore';
const router = useRouter();
const ruleForm = reactive<RuleForm>({
username: '',
password: '',
key: '',
captcha: '',
})
const rules = reactive<FormRules<RuleForm>>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 5, max: 10, message: '位数为5-10位', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 5, max: 10, message: '位数为5-10位', trigger: 'blur' },
],
captcha: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
],
})
const captcha = reactive({
url: ''
})
const getCodeImg = async () => {
// 获取时间戳作为key,防止缓存问题
const key = new Date().getTime().toString();
ruleForm.key = key;
let res = await captchaImage({ key });
let blob = new Blob([res], { type: 'application/vnd.ms-excel' });
let imgUrl = URL.createObjectURL(blob);
captcha.url = imgUrl;
}
const ruleFormRef = ref<FormInstance>()
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async(valid, fields) => {
if (valid) {
const store = useUserStore();
let res = await loginByJson({
username:Encrypt(ruleForm.username),
password:Encrypt(ruleForm.password),
key:ruleForm.key,
captcha:ruleForm.captcha,
});
// 存储token到本地存储中
localStorage.setItem('token', res);
await store.initUserInfoAndConfig();
// 跳转到首页
// router.push('/');
} else {
console.log('error submit!', fields)
}
})
}
onBeforeMount(() => {
getCodeImg();
})
</script>
<style scoped>
.login {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
inset: 0;
background: url('../assets/images/login_background.png') no-repeat center top;
/* background: url('https://luxian-ai.oss-cn-beijing.aliyuncs.com/luxian-ai/avatar/2024-07-28/1722173053591686.jpg') no-repeat center top; */
background-size: cover;
overflow: hidden;
}
.login-form {
padding: 3rem 2rem;
width: 400px;
background-color: #ffffff;
}
.login-title {
width: 100%;
text-align: center;
font-weight: bold;
font-size: 22px;
}
.form-code {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
width: 100%;
}
.code-input {
flex: 1;
}
.code-image {
width: 100px;
height: 40px;
cursor: pointer;
}
.form-submit {
width: 100%;
}
</style>
11, main.ts
import './assets/main.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
// 添加字体图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia())
app.use(router)
app.mount('#app')
12, App.vue
<template>
<RouterView />
</template>
13, vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@router': fileURLToPath(new URL('./src/router', import.meta.url)),
'@stores': fileURLToPath(new URL('./src/stores', import.meta.url)),
'@views': fileURLToPath(new URL('./src/views', import.meta.url)),
'@components': fileURLToPath(new URL('./src/components', import.meta.url)),
'@images': fileURLToPath(new URL('./src/assets/images', import.meta.url)),
'@css': fileURLToPath(new URL('./src/assets/css', import.meta.url)),
'@utils': fileURLToPath(new URL('./src/utils', import.meta.url)),
'@api': fileURLToPath(new URL('./src/api', import.meta.url)),
},
},
// 配置代理 解决跨域问题
server:{
proxy:{
'/api':{
target:'http://uat.admin.banlu.xuexiluxian.cn',
changeOrigin:true,
rewrite:path=>path.replace(/^\/api/,'')
}
}
}
})
安装 sass-embedded
npm install -D sass-embedded
Scrollbar 滚动条
用于替换浏览器原生滚动条。
https://element-plus.org/zh-CN/component/scrollbar.html
以下就是完整代码 src/stores/menuStore.ts
import { defineStore } from 'pinia'
import { getRouters } from '@api/login'
import { useUserStore } from '@stores/userStore'
export const useMenuStore = defineStore('menu-store',{
state: () => ({
// 这里定义你的状态
authSlideMenuMap: null,
authSlideMenuList: null,
}),
actions: {
// 获取用户信息
async loadAuthRouters() {
// console.log('获取路由信息');
const userStore = useUserStore();
const routers = await getRouters(userStore.currentRolePerm);
// console.log(userStore.currentRolePerm);
// console.log(routers);
const slideMenu = normalizeSlideMenu(routers);
this.authSlideMenuMap = slideMenu.authSliseMenuMap;
this.authSlideMenuList = slideMenu.authSlideMenuList;
console.log(this.authSlideMenuMap);
}
}
})
function normalizeSlideMenu(routers) {
const authSliseMenuMap = new Map();
const authSlideMenuList = routers
.map(route => normalizeSlideMenuItem(route.authSlideMenuMap))
.filter(Boolean)
const _authSlideMenuList = [
createMenuRoute({
path:'/',
meta:{
title:'首页'
}
}),
...authSlideMenuList
]
return {
authSliseMenuMap,
authSlideMenuList:_authSlideMenuList
}
}
// 递归生成侧边菜单项 和 路由项
// 将对象里面的数据全部转换成侧边菜单项和路由项处理
function normalizeSlideMenuItem(route, authSliseMenuMap) {
const _route = createMenuRoute(route);
if (route === 0 && route.children) {
_route.children = route.children
.map(item => normalizeSlideMenuItem(item, authSliseMenuMap))
.filter(Boolean)
}
authSliseMenuMap.set(_route.path, _route);
return _route;
}
function createMenuRoute(route) {
return {
path:route.path,
meta:{
...route.meta,
alwaysShow:route.alwaysShow,
hidden:route.hidden,
query:route.query,
redirect:route.redirect,
type:route.type,
}
}
}
导航守卫-权限判断 实现代码
1, src/api/login.ts
// 前端请求后端的所有请求都放在这个文件里统一管理
import http from '@utils/request'
// 图形验证码 获取验证码图片接口
interface ICaptchaImage {
key: string
}
export const captchaImage = (data: ICaptchaImage) => {
return http.get('/captcha/imageCode', data, {responseType:'arraybuffer'})
}
// 用户登录接口
export interface RuleForm {
username: string
password: string
key: string
captcha: string
}
export const loginByJson = (data: RuleForm) => {
return http.post('/u/loginByJson', data)
}
// 个人信息
export const getInfo = () => {
return http.get('/personal/getInfo')
}
// 获取路由信息
export const getRouters = (data) => {
return http.get('/personal/getRouters/${data}')
}
2,src/layout/module/logo/Logo.vue
<script setup>
/*! @file
*******************************************************
<PRE>
文件实现功能 : logo组件
作 者 : mary
版本 : 1.0
-------------------------------------------------------
备注 : -
-------------------------------------------------------
修改记录 :
日期 版本 修改人 修改内容
2025/09/04 1.0 mary 创建
</PRE>
******************************************************/
import logoImgSrc from '@images/logo.png'
// defineOptions({name: ''})
//====================================================
// == 类型定义
//====================================================
// == 初始化
//====================================================
//== 事件处理
</script>
<template>
<transition name="el-fade-in" mode="out-in">
<router-link to="/" class="system-logo">
<el-image :src="logoImgSrc" class="img"></el-image>
<h4>小鹿线基础框架</h4>
</router-link>
</transition>
</template>
<style scoped lang="scss">
.system-logo {
display: flex;
justify-content: center;
align-items: center;
height: 50px;
color: #ffffff;
}
.img {
width: 32px;
height: 32px;
}
h4 {
white-space: nowrap;
margin-left: 10px;
}
</style>
3,src/layout/module/slideMenu/SlideMenuItem.vue
<script setup>
/*! @file
*******************************************************
<PRE>
文件实现功能 : 左侧菜单目录组件
作 者 : mary
版本 : 1.0
-------------------------------------------------------
备注 : -
-------------------------------------------------------
修改记录 :
日期 版本 修改人 修改内容
2025/09/04 1.0 mary 创建
</PRE>
******************************************************/
import { computed } from 'vue'
// defineOptions({name: ''})
//====================================================
// == 类型定义
const props = defineProps({
data: {
type: Object
}
})
const hasChildren = computed(() => {
return Array.isArray(props.data.children) && props.data.children.length > 0;
})
//====================================================
// == 初始化
//====================================================
//== 事件处理
</script>
<template>
<el-sub-menu :index="data.path" v-if="hasChildren">
<template #title :index="data.path">
<el-icon><location /></el-icon>
<span>{{ data.meta.title }}</span>
</template>
<SlideMenuItem
v-for="menu in data.children"
:data="menu"
:key="menu.path"
>
</SlideMenuItem>
</el-sub-menu>
<el-menu-item :index="data.path" v-else>
<el-icon><setting /></el-icon>
<template #title>Navigator Four</template>
</el-menu-item>
</template>
<style scoped>
</style>
4,src/layout/module/slideMenu/SlideMenu.vue
<script setup>
/*! @file
*******************************************************
<PRE>
文件实现功能 : 左侧菜单组件
作 者 : mary
版本 : 1.0
-------------------------------------------------------
备注 : -
-------------------------------------------------------
修改记录 :
日期 版本 修改人 修改内容
2025/09/04 1.0 mary 创建
</PRE>
******************************************************/
// 导入组件模块
import SlideMenuItem from './SlideMenuItem.vue'
// 拿数据,需要导入store模块数据
import { useMenuStore } from '@stores/menuStore'
// defineOptions({name: ''})
//====================================================
// == 类型定义
const store = useMenuStore();
//====================================================
// == 初始化
//====================================================
//== 事件处理
</script>
<template>
<el-scrollbar>
<el-menu
background-color="#545c64"
text-color="#fff"
:unique-opened="true"
>
<SlideMenuItem
v-for="menu in store.authSlideMenuList"
:data="menu"
:key="menu.path"
>
</SlideMenuItem>
</el-menu>
</el-scrollbar>
</template>
<style scoped>
</style>
5,src/layout/SystemLayout.vue
<script setup>
/*! @file
*******************************************************
<PRE>
文件实现功能 : 系统布局组件
作 者 : mary
版本 : 1.0
-------------------------------------------------------
备注 : -
-------------------------------------------------------
修改记录 :
日期 版本 修改人 修改内容
2025/09/04 1.0 mary 创建
</PRE>
******************************************************/
import Logo from './modules/logo/Logo.vue'
import SlideMenu from './modules/slideMenu/SlideMenu.vue'
// defineOptions({name: ''})
//====================================================
// == 类型定义
//====================================================
// == 初始化
//====================================================
//== 事件处理
</script>
<template>
<div class="system">
<!-- 左侧菜单栏 -->
<section class="system-slideMenu" style="width: 200px;">
<Logo />
<SlideMenu />
</section>
<!-- 右侧内容 -->
<div class="system-container">右侧</div>
</div>
</template>
<style scoped>
.system {
display: flex;
flex-direction: row;
}
.system-slideMenu {
/* 固定宽度,不占满剩余空间 */
/* flex: none; */
position: sticky;
top: 0;
left: 0;
z-index: 10;
display: flex;
flex-direction: column;
overflow: hidden;
flex-shrink: 0;
height: 100vh;
background-color: #545c64;
border: 1px solid #545c64;
transition: width 0.6s;
}
.system-container {
/* 占满剩余空间 */
flex: 1;
}
</style>
6, src/router/guards.ts
import { useUserStore } from '@stores/userStore'
export const beforeEach =async (to) => {
if (to.path == '/login') {
document.title = '办鹿后台管理系统';
return ;
}
if (!localStorage.getItem('token')) {
return '/login';
}
try {
// 初始化用户信息与配置信息(路由)
const store = useUserStore();
await store.initUserInfoAndConfig();
}catch(e) {
console.log('error', e);
return '/login';
}
};
export const afterEach = () => {
console.log('后置222');
};
7, src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
// 导航守卫
import { beforeEach, afterEach } from './guards'
// 路由表 路由配置文件
import { AppRoutes } from './routes'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: AppRoutes,
})
router.beforeEach(beforeEach)
router.afterEach(afterEach)
export default router
8, src/router/routes.ts
import { markRaw } from "vue";
export const AppRoutes = markRaw([
{
path: '/login',
name: 'login',
component: () => import('@views/login/Login.vue'),
},
{
path: '/',
name: 'home',
component: () => import('@views/home/Home.vue'),
},
])
9, src/stores/menuStore.ts
import { defineStore } from 'pinia'
import { getRouters } from '@api/login'
import { useUserStore } from '@stores/userStore'
export const useMenuStore = defineStore('menu-store',{
state: () => ({
// 这里定义你的状态
authSlideMenuMap: null,
authSlideMenuList: null,
}),
actions: {
// 获取用户信息
async loadAuthRouters() {
// console.log('获取路由信息');
const userStore = useUserStore();
const routers = await getRouters(userStore.currentRolePerm);
// console.log(userStore.currentRolePerm);
// console.log(routers);
const slideMenu = normalizeSlideMenu(routers);
this.authSlideMenuMap = slideMenu.authSliseMenuMap;
this.authSlideMenuList = slideMenu.authSlideMenuList;
console.log(this.authSlideMenuMap);
}
}
})
function normalizeSlideMenu(routers) {
const authSliseMenuMap = new Map();
const authSlideMenuList = routers
.map(route => normalizeSlideMenuItem(route.authSlideMenuMap))
.filter(Boolean)
const _authSlideMenuList = [
createMenuRoute({
path:'/',
meta:{
title:'首页'
}
}),
...authSlideMenuList
]
return {
authSliseMenuMap,
authSlideMenuList:_authSlideMenuList
}
}
// 递归生成侧边菜单项 和 路由项
// 将对象里面的数据全部转换成侧边菜单项和路由项处理
function normalizeSlideMenuItem(route, authSliseMenuMap) {
const _route = createMenuRoute(route);
if (route === 0 && route.children) {
_route.children = route.children
.map(item => normalizeSlideMenuItem(item, authSliseMenuMap))
.filter(Boolean)
}
authSliseMenuMap.set(_route.path, _route);
return _route;
}
function createMenuRoute(route) {
return {
path:route.path,
meta:{
...route.meta,
alwaysShow:route.alwaysShow,
hidden:route.hidden,
query:route.query,
redirect:route.redirect,
type:route.type,
}
}
}
10, src/stores/userStores.ts
import { defineStore } from 'pinia'
import { getInfo } from '@api/login'
import { useMenuStore } from '@stores/menuStore';
export const useUserStore = defineStore('user-store',{
state: () => ({
// 这里定义你的状态
userInfo: null,
permissions: null,
roles: [],
units: null,
currentRolePerm: sessionStorage.getItem('currentRolePerm') ?? "",
}),
actions: {
// 获取用户信息
async initUserInfoAndConfig() {
// 请求获取个人信息的接口
if (!this.userInfo) {
let {permissions,roles,units,userInfo} = await getInfo();
this.userInfo = userInfo;
this.permissions = permissions;
this.roles = roles;
this.units = units;
if (!this.currentRolePerm) {
this.toggleCurrentRolePerm(roles[0].rolePerm);
}
}
// 获取该用户的路由
await useMenuStore().loadAuthRouters();
},
toggleCurrentRolePerm(rolePerm) {
this.currentRolePerm = rolePerm;
sessionStorage.setItem('currentRolePerm',rolePerm);
}
}
})
11, src/utils/aes.ts
// 引入AES源码js
import CryptoJS from 'crypto-js'
// 默认的KEY与IV如果没有给
// 秘钥: bGvnMc62sh5RV6zP
// 偏移量: 1eZ43DLcYtV2xb3Y
const key = CryptoJS.enc.Utf8.parse('bGvnMc62sh5RV6zP')
// 十六位十六进制数作为秘钥
const iv = CryptoJS.enc.Utf8.parse('1eZ43DLcYtV2xb3Y')
// 十六位十六进制数作为秘钥偏移量
// 解密方法
export function Decrypto(word) {
const encryptedHexStr = CryptoJS.enc.Hex.parse(word)
const srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr)
const decrypt = CryptoJS.AES.decrypt(srcs, key, {iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7})
const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8)
return decryptedStr.toString()
}
// 加密方法
export function Encrypt(word) {
const srcs = CryptoJS.enc.Utf8.parse(word)
const encrypted = CryptoJS.AES.encrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 })
return encrypted.ciphertext.toString().toUpperCase()
}
12, src/utils/request.ts
import axios from 'axios';
import { ElMessage } from 'element-plus';
// axios.get('/api/data');
// axios.post('/api/data');
// 1. 创建 axios 对象
const request = axios.create({
baseURL: '/api', // 你的后端接口地址
timeout: 5000 // 请求超时时间
});
// 2. 添加请求拦截器(前端请求后端,前端给后端传数据时,可以设置一些拦截器,比如token之类的信息)
request.interceptors.request.use(function(config) {
return config;
})
// 3. 添加响应拦截器(后端给前端返回数据,前端接收数据时,可以设置一些拦截器,比如token之类的信息)
request.interceptors.response.use(function(response) {
if (response.data instanceof ArrayBuffer) {
return response.data;
}
let { code, msg, data } = response.data;
if (code == 200) {
return data;
}
if (msg) {
ElMessage({type: 'error',message: msg})
}
// 捕获异常信息
throw msg;
})
// 4. 封装请求方法 (封装成一个对象,方便调用) get post
const http = {
get(url, params, config) {
return new Promise((resolve, reject) => {
request.get(url,{params,...config})
.then(res => {
resolve(res)
})
.catch(error => {
reject(error)
})
})
},
post(url, data, config) {
return new Promise((resolve, reject) => {
request.post(url,{data,config})
.then(res => {
resolve(res)
})
.catch(error => {
reject(error)
})
})
}
}
export default http;
13, src/views/home/Home.vue
<script setup>
/*! @file
*******************************************************
<PRE>
文件实现功能 : 首页
作 者 : mary
版本 : 1.0
-------------------------------------------------------
备注 : -
-------------------------------------------------------
修改记录 :
日期 版本 修改人 修改内容
2025/9/04 1.0 mary 创建
</PRE>
******************************************************/
import SystemLayout from '@layout/SystemLayout.vue'
// defineOptions({name: ''})
//====================================================
// == 类型定义
//====================================================
// == 初始化
//====================================================
//== 事件处理
</script>
<template>
<SystemLayout>
123
</SystemLayout>
</template>
<style scoped>
</style>
14, src/views/login/Login.vue
<template>
<div class="login">
<el-form class="login-form" :module="ruleForm" :rules="rules" ref="ruleFormRef">
<el-form-item>
<h2 class="login-title">小鹿线基础框架</h2>
</el-form-item>
<el-form-item label="用户名:" prop="username">
<el-input size="large" placeholder="用户名" v-model="ruleForm.username">
<template #prefix>
<el-icon><User /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="密码:" prop="password">
<el-input size="large" placeholder="密码" show-password v-model="ruleForm.password">
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="验证码:" prop="captcha">
<div class="form-code">
<el-input class="code-input" size="large" placeholder="验证码" :maxlength="4" show-word-limit v-model="ruleForm.captcha">
<template #prefix>
<el-icon><Aim /></el-icon>
</template>
</el-input>
<el-image class="code-image" :src="captcha.url" @click="getCodeImg"></el-image>
</div>
</el-form-item>
<el-form-item>
<el-checkbox>记住密码</el-checkbox>
</el-form-item>
<el-form-item>
<el-button size="large" type="primary" class="form-submit" @click="submitForm(ruleFormRef)">登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onBeforeMount } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { captchaImage, RuleForm, loginByJson } from '@api/login';
import { Encrypt } from '@utils/aes';
import { useRouter } from 'vue-router';
import { useUserStore } from '@stores/userStore';
const router = useRouter();
const ruleForm = reactive<RuleForm>({
username: '',
password: '',
key: '',
captcha: '',
})
const rules = reactive<FormRules<RuleForm>>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 5, max: 10, message: '位数为5-10位', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 5, max: 10, message: '位数为5-10位', trigger: 'blur' },
],
captcha: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
],
})
const captcha = reactive({
url: ''
})
const getCodeImg = async () => {
// 获取时间戳作为key,防止缓存问题
const key = new Date().getTime().toString();
ruleForm.key = key;
let res = await captchaImage({ key });
let blob = new Blob([res], { type: 'application/vnd.ms-excel' });
let imgUrl = URL.createObjectURL(blob);
captcha.url = imgUrl;
}
const ruleFormRef = ref<FormInstance>()
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async(valid, fields) => {
if (valid) {
const store = useUserStore();
let res = await loginByJson({
username:Encrypt(ruleForm.username),
password:Encrypt(ruleForm.password),
key:ruleForm.key,
captcha:ruleForm.captcha,
});
// 存储token到本地存储中
localStorage.setItem('token', res);
await store.initUserInfoAndConfig();
// 跳转到首页
// router.push('/');
} else {
console.log('error submit!', fields)
}
})
}
onBeforeMount(() => {
getCodeImg();
})
</script>
<style scoped>
.login {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
inset: 0;
background: url('../assets/images/login_background.png') no-repeat center top;
/* background: url('https://luxian-ai.oss-cn-beijing.aliyuncs.com/luxian-ai/avatar/2024-07-28/1722173053591686.jpg') no-repeat center top; */
background-size: cover;
overflow: hidden;
}
.login-form {
padding: 3rem 2rem;
width: 400px;
background-color: #ffffff;
}
.login-title {
width: 100%;
text-align: center;
font-weight: bold;
font-size: 22px;
}
.form-code {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
width: 100%;
}
.code-input {
flex: 1;
}
.code-image {
width: 100px;
height: 40px;
cursor: pointer;
}
.form-submit {
width: 100%;
}
</style>
15, main.ts
// 重置 css 样式
import '@css/custom.init.css'
import '@css/init.css'
// import './assets/main.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
// 添加字体图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia())
app.use(router)
app.mount('#app')
16, App.vue
<template>
<RouterView />
</template>
17, vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@router': fileURLToPath(new URL('./src/router', import.meta.url)),
'@stores': fileURLToPath(new URL('./src/stores', import.meta.url)),
'@views': fileURLToPath(new URL('./src/views', import.meta.url)),
'@components': fileURLToPath(new URL('./src/components', import.meta.url)),
'@images': fileURLToPath(new URL('./src/assets/images', import.meta.url)),
'@css': fileURLToPath(new URL('./src/assets/css', import.meta.url)),
'@utils': fileURLToPath(new URL('./src/utils', import.meta.url)),
'@api': fileURLToPath(new URL('./src/api', import.meta.url)),
'@layout': fileURLToPath(new URL('./src/layout', import.meta.url)),
},
},
// 配置代理 解决跨域问题
server:{
proxy:{
'/api':{
target:'http://uat.admin.banlu.xuexiluxian.cn/api',
changeOrigin:true,
rewrite:path=>path.replace(/^\/api/,'')
}
}
}
})
18, index.html
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>