中文官网
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: stringpassword: stringkey: stringcaptcha: 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) returnawait 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) returnawait 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: stringpassword: stringkey: stringcaptcha: 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-jsnpm 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) returnawait 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, 登录成功,获取到token2, 请求个人信息接口,把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: stringpassword: stringkey: stringcaptcha: 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 router4, 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
// 偏移量: 1eZ43DLcYtV2xb3Yconst 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) returnawait 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: stringpassword: stringkey: stringcaptcha: 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><SlideMenuItemv-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-menubackground-color="#545c64"text-color="#fff":unique-opened="true"><SlideMenuItemv-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.tsimport { 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 router8, 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
// 偏移量: 1eZ43DLcYtV2xb3Yconst 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) returnawait 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>