Element-Plus

发布于:2025-09-07 ⋅ 阅读:(18) ⋅ 点赞:(0)
中文官网
https://element-plus.org/zh-CN/
第一步:安装 Element-plus
npm install element-plus --save
第二步: 安装 首先你需要安装unplugin-vue-componentsunplugin-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>


网站公告

今日签到

点亮在社区的每一天
去签到