基于Java的仓库管理系统设计与实现(五)

目前正在编写前端界面,后端接口写了七七八八,在编写前端与后端交互中再进行改动。
前端gitee地址

1. 创建Vue项目

使用vite创建vue项目,并引入pinia持久化插件,还有Element-Plus,按需导入插件等等。具体配置不再记录,前几天Vue3基础篇已经基本都引入了

2. 编写登录界面、首页等,请求后端接口生成菜单。

写的不好,但是够用,要求不高,实在是对这玩意不敏感,不会写就搜搜查查,总有合适的。

登陆界面 login.vue

<!-- 登陆界面 login.vue -->
<template>
  <el-row>
    <el-col :md="12" :lg="16">
      <div>
        <h1>欢迎使用</h1>
        <h2>欢迎使用 基于Java的仓库后台管理系统</h2>
        <h2>作者:十一月</h2>
      </div>
    </el-col>
    <el-col :md="12" :lg="8">
      <div v-if="isLoginForm">
        <h2>欢迎回来</h2>
        <span class="line-text"> 账号密码登录 </span>
        <el-form ref="formRef" style="max-width: 35vh" :model="ruleForm" status-icon :rules="rules" label-width="20%">
          <el-form-item label="账号" prop="account">
            <el-input v-model="ruleForm.account" type="text" placeholder="请输入账号" />
          </el-form-item>
          <el-form-item label="密码" prop="password">
            <el-input v-model="ruleForm.password" type="password" placeholder="请输入密码" show-password />
          </el-form-item>
          <el-form-item>
            <el-checkbox v-model="rememberPassword">记住密码</el-checkbox>
            <span style="cursor: pointer" @click="switchToRegister">还没有账号,去注册</span>
          </el-form-item>
          <el-form-item>
            <el-button round style="width: 35vh; font-size: 1.5vh;" type="primary" @click="submitForm">
              登录
            </el-button>
          </el-form-item>
        </el-form>
      </div>
      <div v-else=>
        <h2>请先注册</h2>
        <el-form ref="registerFormRef" style="max-width: 35vh" :model="registerForm" status-icon :rules="registerRules"
          label-width="30%">
          <el-form-item label="注册账号" prop="registerAccount">
            <el-input v-model="registerForm.registerAccount" type="text" placeholder="请输入注册账号" />
          </el-form-item>
          <el-form-item label="注册密码" prop="registerPassword">
            <el-input v-model="registerForm.registerPassword" type="password" placeholder="请输入注册密码" show-password />
          </el-form-item>
          <el-form-item label="确认密码" prop="registerReconfirmPassword">
            <el-input v-model="registerForm.registerReconfirmPassword" @blur="checkRegisterPassword" type="password"
              placeholder="请再次输入注册密码" show-password />
          </el-form-item>
          <el-form-item label="姓名" prop="name">
            <el-input v-model="registerForm.name" type="text" placeholder="请输入真实姓名" />
          </el-form-item>
          <el-form-item label="手机号" prop="phone">
            <el-input v-model="registerForm.phone" type="text" placeholder="请输入注册手机号" maxlength="11" />
          </el-form-item>
          <el-form-item label="邮箱" prop="email">
            <el-input v-model="registerForm.email" type="text" placeholder="请输入注册邮箱" />
          </el-form-item>
          <el-form-item label="验证码" prop="verificationCode">
            <el-input style="width: 13vh;margin-right: 2vh;" v-model="registerForm.verificationCode" type="text"
              placeholder="邮箱验证码" />
            <el-button type="primary" @click="sendVerificationCode" :disabled="isSendingCode">{{ sendText }}</el-button>
          </el-form-item>
          <el-form-item>
            <span style="cursor: pointer" @click="switchToLogin">已有账号,去登录</span>
          </el-form-item>
          <el-form-item>
            <el-button round style="width: 35vh; font-size: 1.5vh; " type="primary" @click="submitRegisterForm">
              注册
            </el-button>
          </el-form-item>
        </el-form>
      </div>
    </el-col>
  </el-row>
</template>

<script setup>
  import { useUserStore } from '@/store/user.js';
  import { useRouter } from 'vue-router';
  import request from '@/utils/request.js';

  // 获取 router 实例  编程式导航使用
  const router = useRouter();
  // 获取 userStore 实例
  const userStore = useUserStore();


  // 表单组件
  const formRef = ref(null);
  // 注册表单组件
  const registerFormRef = ref(null);
  // 记住密码
  const rememberPassword = ref(false);
  // 是否是登录表单
  const isLoginForm = ref(true);
  // 是否正在发送验证码
  const isSendingCode = ref(false);
  // 发送验证码倒计时
  const countdown = ref(0);
  // 发送验证码按钮文字
  const sendText = ref('发送');

  // 登录表单字段
  const ruleForm = reactive({
    account: '',
    password: '',
  });

  // 注册表单字段
  const registerForm = reactive({
    registerAccount: '',
    registerPassword: '',
    registerReconfirmPassword: '',
    name: '',
    phone: '',
    email: '',
    verificationCode: ''
  });
  //返回注册
  const switchToRegister = () => {
    isLoginForm.value = false;
  };

  //返回登陆
  const switchToLogin = () => {
    isLoginForm.value = true;
  };


  // 自定义密码匹配校验函数
  const validatePasswordMatch = (rule, value, callback) => {
    if (value !== registerForm.registerPassword) {
      callback(new Error('两次输入的密码不一致,请重新输入'));
    } else {
      callback();
    }
  };

  // 手机号校验规则
  const validatePhone = (rule, value, callback) => {
    const phoneReg = /^1[3-9]\d{9}$/;
    if (!phoneReg.test(value)) {
      callback(new Error('请输入有效的手机号'));
    } else {
      callback();
    }
  };

  // 邮箱校验规则
  const validateEmail = async (rule, value, callback) => {
    const emailReg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
    if (!emailReg.test(value)) {
      callback(new Error('请输入有效的邮箱地址'));
    } else {
      //通过校验,判断邮箱是否存在
      try {
        const response = await request.post('auth/checkUser', {
          email: value
        });

        if (response.code === 200) {
          callback();
        } else {
          callback(new Error(response.message));
        }
      } catch (error) {
        callback(new Error(response.message));
      }
    }
  };

  // 注册账号是否存在校验函数
  const checkUserExists = async (rule, value, callback) => {
    try {
      const response = await request.post('auth/checkUser', {
        username: value
      });

      if (response.code === 200) {
        callback();
      } else {
        callback(new Error(response.message));
      }
    } catch (error) {
      callback(new Error(response.message));
    }
  };


  // 登录表单校验规则
  const rules = {
    account: [
      { required: true, message: '请输入账号', trigger: 'blur' },
      { min: 4, max: 20, message: '长度在 4 到 20 个字符', trigger: 'blur' },
    ],
    password: [
      { required: true, message: '请输入密码', trigger: 'blur' },
      { min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' },
    ],
  };
  // 注册表单校验规则
  const registerRules = {
    registerAccount: [
      { required: true, message: '请输入注册账号', trigger: 'blur' },
      { min: 4, max: 20, message: '长度在 4 到 20 个字符', trigger: 'blur' },
      // 关联校验函数
      { validator: checkUserExists, trigger: 'blur' },
    ],
    registerPassword: [
      { required: true, message: '请输入注册密码', trigger: 'blur' },
      { min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' },
    ],
    registerReconfirmPassword: [
      { required: true, message: '请再次输入注册密码', trigger: 'blur' },
      { min: 6, max: 20, message: '长度在 6 到 20 个字符,和注册密码保持一致', trigger: 'blur' },
      // 关联校验函数
      { validator: validatePasswordMatch, trigger: 'blur' },
    ],
    name: [
      { required: true, message: '请输入真实姓名', trigger: 'blur' }
    ],
    phone: [
      { required: true, message: '请输入注册手机号', trigger: 'blur' },
      // 关联校验函数
      { validator: validatePhone, trigger: 'blur' }
    ],
    email: [
      { required: true, message: '请输入注册邮箱', trigger: 'blur' },
      // 关联校验函数
      { validator: validateEmail, trigger: 'blur' }
    ],
    verificationCode: [
      { required: true, message: '请输入邮箱收到的验证码', trigger: 'blur' }
    ]
  };

  // 登陆表单提交
  const submitForm = () => {
    formRef.value.validate((valid) => {
      if (valid) {
        handleLogin();
        // 这里可以添加登录成功后的逻辑
      }
    });
  };

  // 注册表单提交
  const submitRegisterForm = () => {
    registerFormRef.value.validate((valid) => {
      if (valid) {
        handleRegister();
        // 这里可以添加注册成功后的逻辑
      }
    });
  };

  // 检测二次密码是否一致  失去焦点触发校验规则
  const checkRegisterPassword = () => {
    registerFormRef.value.validateField('registerReconfirmPassword');
  };


  //发送邮箱验证码
  const sendVerificationCode = () => {
    if (isSendingCode.value) return;
    // 校验邮箱是否有效
    registerFormRef.value.validateField('email', (valid) => {
      if (valid) {
        handleEmailCode()

        // 这里需要添加实际发送验证码的逻辑,比如调用 API
        console.log(`发送验证码到邮箱: ${registerForm.email}`);
      }
    });
  };

  // 发送邮箱验证码处理
  const handleEmailCode = async () => {
    //验证通过  请求api发送验证码
    try {
      const req = await request.post('auth/sendEmailCode', {
        email: registerForm.email
      });
      console.log(req);
      if (req.code !== 200) {
        showMessage('error', req.message);
      } else {

        //等于200  开始验证码处理
        isSendingCode.value = true;
        countdown.value = 60;
        const timer = setInterval(() => {
          countdown.value--;
          sendText.value = `(${countdown.value})`;
          if (countdown.value === 0) {
            clearInterval(timer);
            isSendingCode.value = false;
            sendText.value = '发送';
          }
        }, 1000);
        showMessage('success', req.message);
      }
    } catch (error) {
      showMessage('error', req.message);
    }
  }

  //登录处理
  const handleLogin = async () => {
    try {
      const response = await request.post('auth/login', {
        username: ruleForm.account,
        password: ruleForm.password
      });


      if (response.code === 200) {
        const token = response.access_token;
        ElMessage({
          type: 'success',
          message: '登录成功',
          duration: 2000,
          showClose: true,
          onClose: () => {
            //登录成功消失后再跳转到首页
            if (rememberPassword.value) {
              // 记住密码了  就存到 pinia 的 store 里
              userStore.setUsers({
                account: ruleForm.account,
                password: ruleForm.password,
                token: token
              });
            } else {
              localStorage.removeItem('user');
            }
            router.push('/index');
          }
        });
      } else {
        showMessage('error', '登录失败,' + response.message);
      }
    } catch (error) {
      showMessage('error', '登录失败' + error.message);
    }
  }
  //注册处理
  const handleRegister = async () => {
    try {
      const response = await request.post('auth/register', {
        username: registerForm.registerAccount,
        password: registerForm.registerPassword,
        realName: registerForm.name,
        phone: registerForm.phone,
        email: registerForm.email,
        code: registerForm.verificationCode
      });

      if (response.code === 200) {
        ElMessage({
          type: 'success',
          message: '注册成功',
          duration: 2000,
          showClose: true
        });
      } else {
        showMessage('error', response.message);
      }
    } catch (error) {
      showMessage('error', response.message);
    }
  }



  // 封装消息提示函数,提高代码复用性
  const showMessage = (type, message) => {
    ElMessage({
      type,
      message,
      duration: 2000,
      showClose: true
    });
  };

  // 挂载后触发记住密码输入账号密码
  onMounted(() => {
    const storedAccount = userStore.users.account;
    const storedPassword = userStore.users.password;
    if (storedAccount && storedPassword) {
      ruleForm.account = storedAccount;
      ruleForm.password = storedPassword;
      rememberPassword.value = true;
    }
  });
</script>

<style scoped>
  .el-row {
    min-height: 100vh;
    min-width: 100vh;
    background-color: rgb(113, 125, 228);
  }

  .el-col {
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .el-col:nth-child(2) {
    background-color: #fff;
    text-align: center;
  }

  h1 {
    font-size: 5vh;
    color: white;
  }

  .line-text {
    position: relative;
    display: inline-block;
    font-size: 2vh;
    color: gray;
    padding: 0 10px;
  }

  .line-text::before,
  .line-text::after {
    content: '';
    position: absolute;
    top: 50%;
    width: 50px;
    height: 1px;
    background-color: gray;
  }

  .line-text::before {
    right: 100%;
  }

  .line-text::after {
    left: 100%;
  }

  h2 {
    font-size: 3vh;
  }

  .el-form {
    margin-top: 3vh;
  }


  .el-checkbox {
    margin-right: 1vh;
  }
</style>

首页 index.vue

<template>
  <el-container class="layout-container-demo" style="height: 100vh">
    <el-aside width="200px">
      <el-scrollbar>
        <AsideMenu :menus="menus" @select="handleMenuSelect" @home-click="switchToHome"
          @item-click="handleMenuItemClick" />
      </el-scrollbar>
    </el-aside>

    <el-container>
      <el-header style="text-align: right; font-size: 18px">
        <div class="toolbar">
          <el-dropdown @command="handleUserDropdownCommand">
            <div class="user-account" @mouseenter="showUserMenu = true" @mouseleave="showUserMenu = false">
              <p>{{ userStore.users.account }}</p>
            </div>
            <template #dropdown>
              <el-dropdown-menu>
                <el-dropdown-item command="profile">个人资料</el-dropdown-item>
                <el-dropdown-item command="settings">设置</el-dropdown-item>
                <el-dropdown-item command="logout">退出登录</el-dropdown-item>
              </el-dropdown-menu>
            </template>

          </el-dropdown>
        </div>
      </el-header>

      <el-main>
        <el-tabs v-model="activeTabName" type="card" class="demo-tabs" @edit="handleTabsEdit">
          <el-tab-pane v-for="tab in tabs" :key="tab.name" :label="tab.title" :name="tab.name" :closable="tab.closable">
            <div class="tab-content">
              <p>{{ tab.title }} 的内容</p>
              <!-- 你的页面内容 -->
            </div>
          </el-tab-pane>
        </el-tabs>
      </el-main>
      <el-footer>
        <div class="footer-content">
          © 2025 十一月 Copyright
        </div>
      </el-footer>
    </el-container>
  </el-container>
</template>

<script setup>
  import { useUserStore } from '@/store/user';
  import { useRouter } from 'vue-router';
  import request from '@/utils/request.js';
  import AsideMenu from '@/views/asideMenu.vue';
  const router = useRouter();
  const userStore = useUserStore();

  const item = {
    date: '2016-05-02',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
  };
  const tableData = ref(Array.from({ length: 20 }).fill(item));

  // 菜单配置
  const menus = ref([]);

  // 标签页数据
  const tabs = ref([
    {
      title: '首页',
      name: '0',
      closable: false,
    },
  ]);
  let tabIndex = 0
  const activeTabName = ref('0');
  const showUserMenu = ref(false);

  // 菜单交互处理
  const handleMenuSelect = (index) => {
    if (index === 'close') {
      handleSelect("close")
    }
  };

  const handleMenuItemClick = (item) => {
    addTab(item.menuName, item.id);
  };

  // 添加首页跳转方法
  const switchToHome = () => {
    activeTabName.value = '0';
  };

  // 添加标签页的方法
  const addTab = (title, name) => {
    const existingTab = tabs.value.find(tab => tab.name === name);
    if (!existingTab) {
      tabs.value.push({
        title,
        name,
        closable: true,
      })
    }
    activeTabName.value = name
  };


  // 标签页编辑事件(修改后)
  const handleTabsEdit = (targetName, action) => {
    if (action === 'remove') {
      // 防止删除首页
      if (targetName === '0') return;

      const index = tabs.value.findIndex(tab => tab.name === targetName);
      if (index > -1) {
        tabs.value.splice(index, 1);
      }
      if (activeTabName.value === targetName) {
        const lastTab = tabs.value[tabs.value.length - 1];
        if (lastTab) {
          activeTabName.value = lastTab.name;
        }
      }
    }
  };

  // 用户下拉菜单命令处理
  const handleUserDropdownCommand = (command) => {
    switch (command) {
      case 'profile':
        console.log('查看个人资料');
        break;
      case 'settings':
        console.log('打开设置');
        break;
      case 'logout':
        console.log('退出登录');
        handleSelect("close")
        break;
      default:
        break;
    }
  };


  // 处理菜单选中回调
  const handleSelect = (index) => {
    console.log(index)
    if (index == "close") {
      ElMessageBox.confirm('确认退出系统吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        ElMessage({
          type: 'success',
          message: '退出成功',
          duration: 1000,
          showClose: true,
          onClose: () => {
            //清除token
            userStore.resetToken();
            //登录成功消失后再跳转到首页
            router.push('/login');
          }
        });
      }).catch(() => {
        showMessage('info', '取消退出')
      });
    }
  }

  const getMenuList = async () => {
    try {
      const response = await request.post('system/menu/list', {
        roleId: 1
      });

      if (response.code === 200) {
        const data = response.data;
        const menuList = data;
        menus.value = menuList;
        showMessage('success', response.message);

      } else {
        showMessage('error', response.message);
      }
    } catch (error) {
      showMessage('error', '获取菜单失败');
    }
  }
  // 封装消息提示函数,提高代码复用性
  const showMessage = (type, message) => {
    ElMessage({
      type,
      message,
      duration: 2000,
      showClose: true
    });
  };
  onMounted(() => {
    getMenuList();
  })

</script>

<style>
  /* 定义全局颜色变量 */
  :root {
    --global-bg-color: #FFFFFF;
    /* 深蓝色背景 */
    --aside-bg-color: #F9FAFC;
    /* 侧边栏背景色 */
    --aside-active-bg-color: #3169a1;
    /* 侧边栏展开背景色 */
    --header-bg-color: #605CA8;
    /* 头部背景色 */
    --footer-bg-color: #FFFFFF;
    /* 底部背景色 */
    --main-bg-color: #FFFFFF;
    /* 浅灰色主内容区背景色 */
    --text-color: #000;
    /* 文本颜色 */
  }

  /* 整体布局容器 */
  .layout-container-demo {
    display: flex;
    height: 100vh;
    background-color: var(--global-bg-color);
  }

  .layout-container-demo .el-aside {
    color: var(--text-color);
    background-color: var(--aside-bg-color);
  }

  .layout-container-demo .custom-menu {
    background-color: var(--aside-bg-color);
    border-right: none;
  }

  .layout-container-demo .custom-menu .el-sub-menu__title,
  .el-menu-item-group__title,
  .el-menu-item {
    background-color: var(--aside-bg-color);
    color: var(--text-color);
  }

  .layout-container-demo .custom-menu .el-sub-menu__title .el-menu-item-group__title .el-menu-item {
    background-color: var(--aside-active-bg-color);
    color: var(--text-color);
  }

  .layout-container-demo .custom-menu .el-sub-menu__popper {
    background-color: var(--aside-bg-color);
  }


  .layout-container-demo .el-header {
    position: relative;
    background-color: var(--header-bg-color);
    color: var(--text-color);
    height: 8%;
  }

  .layout-container-demo .el-main {
    padding: 0;
    flex: 1;
    display: flex;
    flex-direction: column;
    background-color: var(--main-bg-color);
  }

  .layout-container-demo .el-scrollbar {
    flex: 1;
  }

  .layout-container-demo .toolbar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 100%;
  }

  .layout-container-demo .el-footer {
    background-color: var(--footer-bg-color);
    display: flex;
    justify-content: center;
    align-items: center;
    height: 4%;
    color: var(--text-color);
  }

  .layout-container-demo .custom-tabs {
    flex: 1;
    background-color: var(--main-bg-color);
    color: #6b778c;
    font-size: 32px;
    font-weight: 600;
  }

  .custom-tabs span {
    vertical-align: middle;
    margin-left: 4px;
  }

  .layout-container-demo .el-tabs__header {
    background-color: var(--main-bg-color);
  }

  .layout-container-demo .el-tabs__item.is-active {
    background-color: var(--active-tab-bg-color);
  }

  .layout-container-demo .user-account {
    display: inline-flex;
    align-items: center;
    padding: 0 10px;
    cursor: pointer;
    color: #fff;
  }
</style>

菜单页 asideMenu.vue

<template>
    <el-menu :default-openeds="['1']" class="custom-menu" @select="handleMenuSelect">
        <el-menu-item index="dashboard"
            style="justify-content: center;background-color: #555299; min-height: 8vh; font-size: 24px;"
            @click="handleHomeClick">仓库管理系统</el-menu-item>

        <template v-for="menu in menus" :key="menu.id">
            <el-sub-menu v-if="menu.children && menu.children.length > 0" :index="menu.id.toString()">
                <template #title>
                    <el-icon v-if="menu.icon">
                        <component :is="$icon[menu.icon]" />
                    </el-icon>
                    {{ menu.menuName }}
                </template>
                <el-menu-item-group>
                    <el-menu-item v-for="child in menu.children" :key="child.id" :index="child.id.toString()"
                        @click="() => handleMenuItemClick(child)">
                        <el-icon v-if="child.icon">
                            <component :is="$icon[child.icon]" />
                        </el-icon>
                        {{ child.menuName }}
                    </el-menu-item>
                </el-menu-item-group>
            </el-sub-menu>

            <el-menu-item v-else :index="menu.id.toString()" @click="() => handleMenuItemClick(menu)">
                <template #title>
                    <el-icon v-if="menu.icon">
                        <component :is="$icons[menu.icon]" />
                    </el-icon>
                    {{ menu.menuName }}
                </template>
            </el-menu-item>
        </template>

        <el-menu-item index="close">
            <template #title>
                <el-icon>
                    <iEpSwitchButton />
                </el-icon>退出系统
            </template>
        </el-menu-item>
    </el-menu>
</template>

<script setup>
    import { defineProps, defineEmits } from 'vue';

    const props = defineProps({
        menus: {
            type: Array,
            required: true
        }
    });

    const emits = defineEmits(['select', 'home-click', 'item-click']);

    const handleMenuSelect = (index) => {
        emits('select', index);
    };

    const handleHomeClick = () => {
        emits('home-click');
    };

    const handleMenuItemClick = (item) => {
        emits('item-click', item);
    };
</script>

3. 封装Axios请求

src下新建utils文件夹,再创建request.js

import axios from 'axios';

// 创建 axios 实例
const service = axios.create({
    baseURL: import.meta.env.VUE_APP_BASE_API, // 确保此处正确
    timeout: 5000 // 请求超时时间
});

const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE3NDIyODQ1NjMsImV4cCI6MTc0MjM3MDk2MywidXNlcklkIjo1LCJpYXQiOjE3NDIyODQ1NjMsInVzZXJuYW1lIjoiZGFoYWkifQ.bTTJq1jod6gvFYElcyswNf6XkKflUqFW8YzTLJWJ738';
        
// 请求拦截器
service.interceptors.request.use(
    config => {
        // 在发送请求之前做些什么
        // 例如,添加请求头
        // 添加token  先固定 后续从userStore中获取
        config.headers['token'] = token;
        return config;
    },
    error => {
        // 处理请求错误
        console.log(error); // for debug
        Promise.reject(error);
    }
);

// 响应拦截器
service.interceptors.response.use(
    response => {
        const res = response.data;
        // // 这里可以根据业务需求处理响应数据
        // if (res.code !== 200) {
        //     // 处理错误情况
        //     console.error(res.message);
        //     return Promise.reject(new Error(res.message || 'Error'));
        // } else {
        //     return res;
        // }
        return res;
    },
    error => {
        // 处理响应错误
        console.log('err' + error); // for debug
        return Promise.reject(error);
    }
);

// 封装请求方法
const request = {
    get(url, params = {}) {
        return service.get(url, { params });
    },
    post(url, data = {}) {
        return service.post(url, data);
    },
    put(url, data = {}) {
        return service.put(url, data);
    },
    delete(url, params = {}) {
        return service.delete(url, { params });
    }
};

export default request;
    

这里用到了import.meta.env.VUE_APP_BASE_API,确保项目根目录.env文件,我这里是开发环境,所以文件名为.env.development,里面写API地址

# 注意一定要以VUE_APP 开头
VUE_APP_BASE_API=http://localhost:8080`  # 开发环境的 API 地址

配置这里,axios调用的时候,发现请求不对,打印一看没有获取到这个值,百度一顿搜,试了一堆都没有,最终看到了一个需要在vite.config.js进行一个配置,因为vite不识别,默认识别的是VITE_开头的

export default defineConfig({
  //添加这一行就可以了
  envPrefix: ['VITE_', 'VUE_APP_'],
  //....
}

4. 后端邮件发送配置

  1. 引入依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
  1. yml文件配置,我这里使用的网易
spring:
  mail:
    host: smtp.163.com
    port: 25
    username: 邮箱账号
    password: 获取的授权码
    properties:
      mail.smtp.auth: true
      mail.smtp.starttls.enable: true
      mail.smtp.starttls.required: true
  1. 创建EmailVo类
package cn.xy21lin.wms_lin.vo;

import lombok.Data;

@Data
public class EmailVo {
    private String from;
    private String to;
    private String subject;
    private String content;
}

  1. 编写EmailService,对应实现类EmailServiceImpl
package cn.xy21lin.wms_lin.service;

import cn.xy21lin.wms_lin.vo.EmailVo;

public interface EmailService {
    void sendMail(EmailVo email);
}



// 实现类
package cn.xy21lin.wms_lin.service.impl;

import cn.xy21lin.wms_lin.service.EmailService;
import cn.xy21lin.wms_lin.vo.EmailVo;
import jakarta.annotation.Resource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

@Service
public class EmailServiceImpl implements EmailService {

    @Resource
    private JavaMailSender javamail;

    @Override
    public void sendMail(EmailVo email) {
        // 创建一个简单邮件消息对象。
        SimpleMailMessage message = new SimpleMailMessage();
        // 设置发件人地址。
        message.setFrom(email.getFrom());
        // 设置收件人地址。
        message.setTo(email.getTo());
        // 设置邮件主题。
        message.setSubject(email.getSubject());
        // 设置邮件内容。
        message.setText(email.getContent());
        // 设置抄送地址为发件人地址。
        message.setCc(email.getFrom());
        // 使用注入的 JavaMailSender 发送邮件。
        javamail.send(message);
    }
}
  1. 创建MailUtil,添加验证码有效期5分钟、验证验证码、发送邮件逻辑
package cn.xy21lin.wms_lin.util;

import cn.xy21lin.wms_lin.service.EmailService;
import cn.xy21lin.wms_lin.vo.EmailVo;
import jakarta.annotation.Resource;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;

@Component
public class MailUtil {

    @Resource
    private  EmailService emailService;

    private final Map<String, VerificationCodeInfo> verificationCodes = new ConcurrentHashMap<>();

    @Value("${spring.mail.username}")
    private  String from;

    public void sendMail(String email) {
        String code = generateVerificationCode();
        // 记录验证码和过期时间
        verificationCodes.put(email, new VerificationCodeInfo(code, LocalDateTime.now().plusMinutes(5)));

        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("仓库管理系统:")
                .append("\n欢迎注册,您的验证码为:")
                .append(code)
                .append("\n验证码 5 分钟有效,请尽快填写");
        EmailVo emailVo = new EmailVo();
        emailVo.setFrom(from);
        emailVo.setTo(email);
        String format = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
        emailVo.setSubject(format + "-仓库管理系统注册验证码");
        emailVo.setContent(stringBuilder.toString());

        emailService.sendMail(emailVo);
    }

    public boolean verifyCode(String email, String code) {
        VerificationCodeInfo info = verificationCodes.get(email);
        if (info != null) {
            if (LocalDateTime.now().isBefore(info.getExpirationTime()) && info.getCode().equals(code)) {
                verificationCodes.remove(email);
                return true;
            } else {
                // 验证码已过期或不正确,移除记录
                verificationCodes.remove(email);
            }
        }
        return false;
    }

    private String generateVerificationCode() {
        Random random = new Random();
        int code = 100000 + random.nextInt(900000);
        return String.valueOf(code);
    }

    @Getter
    // 内部类,用于存储验证码和过期时间
    private class VerificationCodeInfo {
        private final String code;
        private final LocalDateTime expirationTime;

        public VerificationCodeInfo(String code, LocalDateTime expirationTime) {
            this.code = code;
            this.expirationTime = expirationTime;
        }
    }
}
  1. Controller编写请求

前端发送邮件验证码请求时,总是超时,但是过了十几秒邮件又收到了,所以我用hutool的线程工具类去发,避免请求阻塞等待发送结果,直接返回发送成功

    @Resource
    private MailUtil mailUtil;

    private static final Map<String, Long> emailSendTimeMap = new ConcurrentHashMap<>();
    private static final String EMAIL_REGEX = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
    private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX);

    @PostMapping("/sendEmailCode")
    @Operation(summary = "发送邮箱验证码", description = "根据邮箱账号发送邮箱验证码")
    @ApiResponse(responseCode = "200", description = "发送成功", content = @Content(schema = @Schema(example = "{\"code\": 200, \"message\": \"发送成功\", \"data\": null, \"success\": \"true\"}")))
    public Result<?> sendEmailCode(
            @RequestBody @Schema(example = "{\"email\": \"571497983@qq.com\"}") String email) {
        // 将收到的字符串转JSON 然后获取email属性
        // {"email":"571497983@qq.com"}
        try{
            email = JSONUtil.parseObj(email).getStr("email");
        }catch (JSONException exception){
            return Result.fail().setMessage("JSON解析失败");
        }
        if (!StringUtils.isNullOrEmpty(email) && !EMAIL_PATTERN.matcher(email).matches()) {
            return Result.fail().setMessage("请输入正确的邮箱账号");
        }

        // 检查是否在一分钟内发送过
        long currentTime = System.currentTimeMillis();
        if (emailSendTimeMap.containsKey(email) && currentTime - emailSendTimeMap.get(email) < 60 * 1000) {
            return Result.fail().setMessage("发送频繁,1分钟后重试");
        }

        // 这里发送邮件  前端发送请求,总是超时,但是过了十几秒邮件有发送了,所以我用hutool的线程工具类去发,直接返回发送成功
        String finalEmail = email;
        ThreadUtil.execAsync(()->{
            mailUtil.sendMail(finalEmail);
        });

        // 更新发送时间
        emailSendTimeMap.put(email, currentTime);
        return Result.success().setMessage("发送成功");
    }

5. git推送

add commit push 记得保存