Springboot3+Vue3+MybatisPlus+Vite+TS+ElementPlus搭建网站


趁着寒假充实一下技能…

顺便更新一下自己的技术栈Springboot3+Vue3+MybatisPlus+Vite+TS+ElementPlus

路由守卫

import { jwtDecode } from 'jwt-decode';
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth) { // 检查路由是否需要登录认证
    const token = localStorage.getItem('token');
    const isAuthenticated = token !== null;
    
    if (isAuthenticated) {
      // 如果本地存储中存在 token,则解析 token 并提取过期时间
      try {
        const decodedToken = jwtDecode(token);
        const expirationDate = decodedToken.exp * 1000; // 将过期时间转换为毫秒

        if (Date.now() < expirationDate) {
          next(); // 如果 token 有效,则继续导航
        } else {
          next('/login'); // 如果 token 已过期,则重定向到登录页面
        }
      } catch (error) {
        console.error('Token decoding failed:', error);
        next('/login'); // 如果 token 无效或解码失败,则重定向到登录页面
      }
    } else {
      next('/login'); // 如果用户未登录,则重定向到登录页面
    }
  } else {
    next(); // 如果路由不需要登录认证,则继续导航
  }
});

Mybatis plus

首先禁用所有的名称转换方法,以保持数据库字段名与实体类字段名完全一致:

mybatis-plus.configuration.map-underscore-to-camel-case=false

在典型的 Spring Boot 应用程序中,通常会遵循 MVC(Model-View-Controller)架构模式。在这个模式中,Controller 层负责处理用户请求,Service 层处理业务逻辑,而 MapperRepository 层负责数据访问。下面是这些层级的一般职责:

  • Controller:接收客户端发送的请求,调用 Service 层的方法处理这些请求,然后返回响应。Controller 层是请求的入口点。
  • Service:包含业务逻辑,通常会调用 Mapper/Repository 来访问数据库。Service 层位于 Controller 和 Mapper/Repository 层之间,起到了协调它们的作用。
  • Mapper/Repository:直接与数据库交互,执行 CRUD 操作(创建、读取、更新、删除)。在 MyBatis 中,Mapper 接口定义了与数据库交互的方法。

这里放一个实例:

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;

@Data
@TableName("comweb_users") //指定需要操作的表名
public class Users {
    @TableId // 默认是不会自动生成主码且主码不需要注名
    private String userName;
    // @TableField("passwd") 指定需要操作的字段,如果不写这一行默认是按照下方定义的字段去匹配,写了可以指定一个不同于字段名的列名,不需要指定长度由数据库自己决定
    private String passwd;
    private String ywProf;
    private String asset;

    // 省略 getter 和 setter 方法
    //    userName = models.CharField(max_length=10, primary_key=True)
    //    passwd = models.CharField(max_length=10, null=True, blank=True)
    //    ywProf = models.CharField(max_length=16, null=True, blank=True) # 专业组名字
    //    asset = models.CharField(max_length=10, null=True, blank=True) # 权限
}

在 MyBatis Plus 中,BaseMapper 提供了一些基本的 CRUD 方法,但它不会自动生成特定于字段的查询方法,如 selectByUserNameAndPasswd。这种方法需要您自行实现,要么通过定义 XML 映射文件,要么通过使用 MyBatis Plus 提供的注解或者使用 QueryWrapper 来构建查询。

如果您希望 MyBatis Plus 自动生成这样的方法,您需要使用 @Select 注解或者使用 MyBatis Plus 的 IServiceServiceImpl 来扩展基本的 CRUD 功能。但请注意,即使使用了 IServiceServiceImpl,也不会自动生成特定的方法,如 selectByUserNameAndPasswd。您仍然需要自定义这样的方法。

1. 使用 QueryWrapper 自定义查询:

如果您不想写额外的注解或 XML 映射,可以在服务层使用 QueryWrapper 来构建自定义查询条件。这样,您就可以利用 BaseMapper 提供的 selectOne 方法来执行查询,如前面的示例所示。

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.yourpackage.entity.Users;
import com.example.yourpackage.mapper.UsersMapper;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UsersService {

    @Autowired
    private UsersMapper usersMapper;

    public Users getUserByUserNameAndPasswd(String userName, String passwd) {
        // 创建 QueryWrapper 实例
        QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
        // 设置查询条件
        queryWrapper.eq("userName", userName).eq("passwd", passwd);
        // 执行查询
        return usersMapper.selectOne(queryWrapper);
    }
}

对应mapper

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.yourpackage.entity.Users;

public interface UsersMapper extends BaseMapper<Users> {
    // 这里不需要添加任何额外的方法,因为BaseMapper已经提供了selectOne等CRUD方法
}

对应controller

import com.example.yourpackage.entity.Users;
import com.example.yourpackage.service.UsersService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
public class UsersController {

    @Autowired
    private UsersService usersService;

    @GetMapping("/login")
    public Users getUserByUserNameAndPasswd(@RequestParam String userName, @RequestParam String passwd) {
        return usersService.getUserByUserNameAndPasswd(userName, passwd);
    }
}

2. 使用注解自定义 SQL:

如果您希望在 Mapper 接口中定义自定义方法并使用注解提供 SQL,您可以这样做:

public interface UsersMapper extends BaseMapper<Users> {
    @Select("SELECT * FROM comweb_users WHERE userName = #{userName} AND passwd = #{passwd}")
    Users selectByUserNameAndPasswd(@Param("userName") String userName, @Param("passwd") String passwd);
}

    @Select("<script>" +
            "SELECT * FROM comweb_users WHERE " +
            "<foreach collection='filterOptions' item='option' separator=' OR '>" +
            "   <foreach collection='option.filterValues' item='filter' separator=' OR '>" +
            "       ${option.columnName} = #{filter}" +
            "   </foreach>" +
            "</foreach>" +
            "</script>")
    List<Users> selectUsersByFilterOptions(@Param("filterOptions") List<Map<String, List<String>>> filterOptions);

对应controller

package com.example.comweb.controller;

import com.example.comweb.mapper.UsersMapper;
import com.example.comweb.model.Users;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UsersController {
    @Autowired
    private UsersMapper usersMapper;

    @GetMapping("/api/users/login")
    public String login(@RequestParam String userName, @RequestParam String passwd) {
        Users user = usersMapper.selectByUserNameAndPasswd(userName, passwd);
        if (user != null) {
            return "Login successful!"; // 登录成功
        } else {
            return "Login failed!"; // 登录失败
        }
    }
}

3. 使用 XML 映射文件(plus中可以不使用xml):

如果您更喜欢使用 XML 映射文件,您需要创建一个与 Mapper 接口同名的 XML 文件,在其中定义您的自定义 SQL 语句。然后,确保 MyBatis Plus 能够找到并加载这个 XML 映射文件。

在任何情况下,MyBatis Plus 都不会自动为您生成特定字段的查询方法,您需要手动提供查询逻辑。如果您想避免书写 SQL 语句,应该使用 QueryWrapper 或者 LambdaQueryWrapper 来构建查询条件。

如果你选择使用 MyBatis Plus 的 XML 映射文件而不是注解来定义 SQL 语句,你需要在 mapper 目录下为你的 UsersMapper 接口创建一个对应的 XML 文件。这个文件通常与接口同名,并且放在相同的包路径下。

以下是一个基本的 XML 映射文件例子,它定义了一个通过用户名和密码查询用户的操作:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.yourpackage.mapper.UsersMapper">
    <!-- 定义resultMap,映射数据库列到Java实体属性 -->
    <resultMap id="UserResultMap" type="com.example.yourpackage.entity.Users">
        <id column="id" property="id"/>
        <result column="user_name" property="userName"/>
        <result column="passwd" property="password"/>
        <!-- 其他字段映射 -->
    </resultMap>

    <!-- 通过用户名和密码查询用户的SQL -->
    <select id="selectByUserNameAndPasswd" resultMap="UserResultMap">
        SELECT * FROM users
        WHERE user_name = #{userName} AND passwd = #{password}
    </select>
</mapper>

在这个文件中:

  • <!DOCTYPE> 声明了 MyBatis 的 DTD,这是为了验证 XML 文件的结构。
  • <mapper> 元素的 namespace 属性应该与你的 Mapper 接口的完全限定名匹配。
  • <resultMap> 定义了如何将数据库表的列映射到你的 Java 实体类的属性。id 元素通常用于主键字段,而 result 用于其他字段。
  • <select> 元素定义了一个 SQL 查询语句。id 属性应该与你的 Mapper 接口中的方法名匹配。resultMap 属性指定了返回结果应该如何映射到 Java 对象。

现在在你的 UsersMapper 接口中,你可以定义一个与 XML 文件中的 select 语句相匹配的方法:

import com.example.yourpackage.entity.Users;
import org.apache.ibatis.annotations.Param;

public interface UsersMapper extends BaseMapper<Users> {
    Users selectByUserNameAndPasswd(@Param("userName") String userName, @Param("password") String password);
}

在你的服务层中,你现在可以直接调用这个方法:

@Service
public class UsersService {

    @Autowired
    private UsersMapper usersMapper;

    public Users getUserByUserNameAndPasswd(String userName, String passwd) {
        return usersMapper.selectByUserNameAndPasswd(userName, passwd);
    }
}

确保你的 MyBatis 配置文件已经配置了 mapper XML 文件的路径,否则 MyBatis Plus 找不到这些文件。例如,在 mybatis-config.xml 中,你需要有如下配置:

<configuration>
    <!-- 其他配置... -->
    <mappers>
        <mapper resource="com/example/yourpackage/mapper/UsersMapper.xml"/>
        <!-- 其他mapper文件 -->
    </mappers>
</configuration>

这样,MyBatis Plus 在启动时会加载并解析这些 XML 文件,你就可以在你的应用程序中使用这些自定义的查询了。

4.Websocket通信

使用 JWT 拦截请求时,ws 请求不能和 http 同时处理,一般有两种方法,独立为 ws 写一个拦截器。或者通过 HeaderMapRequestWrapper 对 WebSocket 请求的 HttpServletRequest 进行包装,在包装过程中将 WebSocket 请求头中的 Sec-WebSocket-Protocol 值提取出来并放入一个名为 Authorization 的请求头中。将 WebSocket 请求转变为标准的 HTTP 请求,从而可以通过常规的 HTTP 过滤器来处理认证逻辑。

/**
 * 修改header信息,key-value键值对儿加入到header中
 */
public class HeaderMapRequestWrapper extends HttpServletRequestWrapper {
    private static final Logger LOGGER = LoggerFactory.getLogger(HeaderMapRequestWrapper.class);
    /**
     * construct a wrapper for this request
     *
     * @param request
     */
    public HeaderMapRequestWrapper(HttpServletRequest request) {
        super(request);
    }
    private Map<String, String> headerMap = new HashMap<>();
    /**
     * add a header with given name and value
     *
     * @param name
     * @param value
     */
    public void addHeader(String name, String value) {
        headerMap.put(name, value);
    }
    @Override
    public String getHeader(String name) {
 
        String headerValue = super.getHeader(name);
        if (headerMap.containsKey(name)) {
            headerValue = headerMap.get(name);
        }
        return headerValue;
    }
    /**
     * get the Header names
     */
    @Override
    public Enumeration<String> getHeaderNames() {
        List<String> names = Collections.list(super.getHeaderNames());
        for (String name : headerMap.keySet()) {
            names.add(name);
        }
        return Collections.enumeration(names);
    }
    @Override
    public Enumeration<String> getHeaders(String name) {
 
        List<String> values = Collections.list(super.getHeaders(name));
 
        if (headerMap.containsKey(name)) {
 
            values = Arrays.asList(headerMap.get(name));
        }
        return Collections.enumeration(values);
    }
}
@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException
    {
        /**
         * 通过request的url判断是否为websocket请求
         * 如果为websocket请求,先处理Authorization
         */
        if(request.getRequestURI().contains("/websocket")){
            HeaderMapRequestWrapper requestWrapper = new HeaderMapRequestWrapper((HttpServletRequest) request);
            //修改header信息 将websocket请求中的Sec-WebSocket-Protocol值取出处理后放入认证参数key
            //此处需要根据自己的认证方式进行修改  Authorization 和 Bearer
            requestWrapper.addHeader("Authorization","Bearer "+request.getHeader("Sec-WebSocket-Protocol"));
            request = (HttpServletRequest)  requestWrapper;
        }
        //处理完后就可以走系统正常的登录认证权限认证逻辑了
		//下边就是自己的验证token 登陆逻辑
        LoginUser loginUser = getLoginUser(request);
        ·······
        chain.doFilter(request, response);
    }

5.异步处理

springboot支持异步处理,不影响正常的请求,比如路由守卫的时候可以加一些计算。

@Async

Element Plus

npm install -D unplugin-vue-components unplugin-auto-import

vite.config.ts 按需引入

import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  // ...
  plugins: [
    // ...
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
})

全局配置,既然全面拥抱 ts,我们写ts

<template>
  <el-config-provider :size="size" :z-index="zIndex">
    <div id="app">
      <router-view></router-view>
    </div>
  </el-config-provider>
</template>

<script setup lang="ts">
// 删除 export default,因为使用 script setup 会自动导出组件
//在 Vue 3 中,当你使用 `<script setup>` 语法时,组件的名称通常是由其文件名决定的。
import { ref } from 'vue';

// 使用 ref 创建响应式引用
const zIndex = ref(3000);
const size = ref('small');
</script>

<style scoped>
/* 全局样式... */
</style>

Icon

https://www.jb51.net/article/283664.htm

BackTop

回到顶部实现不难,但是容易被打断,第二滚动后立即点击会被中断,如

const scrollToTop = () => {
  // 先停止当前所有滚动
  window.scrollTo(0, window.scrollY);

  // 稍等片刻后开始平滑滚动到顶部
  setTimeout(() => {
    window.addEventListener('wheel', disableScroll, { passive: false });
    window.scrollTo({
      top: 0,
      behavior: 'smooth'
    });

    const checkIfAtTop = () => {
      if (window.scrollY === 0) {
        window.removeEventListener('wheel', disableScroll);
        window.removeEventListener('scroll', checkIfAtTop);
      }
    };

    window.addEventListener('scroll', checkIfAtTop);
  }, 40); // 延迟40毫秒开始滚动到顶部
};

尝试n多种方法,都不太理想,包括监听滚轮

const disableScroll = (event) => {
  event.preventDefault();
};

window.addEventListener('scroll', disableScroll, { passive: false });

// window.removeEventListener('scroll', disableScroll);

实际上这个bug不是鼠标滚轮导致的,因为通过监听可以发现,鼠标滚动停止后再点击回到顶部会被打断,而此时已经没有了滚动,所以阻止滚动是无效的,查看各大平台网站,我自己的hexo主题,还有淘宝他们的网站都是没有这个bug的,这里面包含了比较复杂的动画处理,而且Vue3和TS对Jquery支持有限,无法很好使用Jquery的animate。最后只能采用最简单朴素的方法,就是延迟一小段时间再回到顶部。255已经是不会触发这个bug的最小时间。

const scrollToTop = () => {
    setTimeout(() => {
    window.scrollTo({
      top: 0,
      behavior: "smooth" // 使用平滑滚动
    });
  
  }, 255); // 延迟
};

样式参考

<transition name="slide">
  <div class="top-scroll" v-if="showScrollTop" @click="scrollToTop">
    <a class="btn-floating" href="#!">
      <font-awesome-icon :icon="['fas', 'arrow-up']" style="color: #fff; font-size: 28px;" />
    </a>
  </div>
</transition>

选用图标如下

import { faArrowUp } from '@fortawesome/free-solid-svg-icons';
library.add(faArrowUp)

sass参考

.top-scroll
  position: fixed
  bottom: -20px
  right: -20px
  z-index: 100
  padding: 10px
  width: 100px 
  height: 100px 


  .btn-floating
    display: flex
    justify-content: center
    align-items: center
    width: 60% 
    height: 60% 
    padding: 0 
    border-radius: 50%
    background: #ff2a4e
    box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.163)
    transition: background 0.5s ease

    &:hover
      background: #ff513a


.slide-enter-active, .slide-leave-active
  transition: all 0.3s ease

.slide-enter-from, .slide-leave-to
  transform: translateY(100%)
  opacity: 0

这里还有一个坑,缓慢转变颜色不能用渐变色用函数也不行。

完整代码

<script setup lang="ts">
import { RouterLink, useRoute } from "vue-router";
import { ref, onMounted, onBeforeUnmount, watchEffect, onUnmounted  } from "vue";
import { library } from '@fortawesome/fontawesome-svg-core'
import $ from 'jquery'
import { faSquareFacebook, faTwitter, faSquareInstagram } from '@fortawesome/free-brands-svg-icons'
import { faArrowUp } from '@fortawesome/free-solid-svg-icons';

// 将图标添加到库中
library.add(faTwitter, faSquareFacebook, faSquareInstagram, faArrowUp)

const route = useRoute();
const isTransparent = ref(false);
const showScrollTop = ref(false);

const scrollToTop = () => {
    setTimeout(() => {
    window.scrollTo({
      top: 0,
      behavior: "smooth" // 使用平滑滚动
    });
  
  }, 255); // 延迟 1000 毫秒(1 秒)执行回到顶部的操作
};

// 监听滚动事件
const handleScroll = () => {
  showScrollTop.value = window.scrollY > 100;
  isTransparent.value = route.path === '/' && window.scrollY < 50;
};

// 注册和注销滚动事件监听器
onMounted(() => {
  window.addEventListener('scroll', handleScroll);
});
onUnmounted(() => {
  window.removeEventListener('scroll', handleScroll);
});

watchEffect(() => {
  // 当路由为'/'时,根据滚动位置设置透明度
  isTransparent.value = route.path === '/' && window.scrollY < 50;
  console.log("Current route path:", route.path);
});

const isMenuOpen = ref(false);

const toggleMenu = () => {
  isMenuOpen.value = !isMenuOpen.value;
};


</script>

<template>
  <section id="header1" class="d-flex align-items-center justify-content-between top-0 left-0">
    <div class="contactEmail">
      Contact Email: <a href="mailto:info@jmymelody.com">info@jmymelody.com</a>
    </div>
    <!-- <img src="../../img/logo.png" class="logo" alt=""> -->
    <nav id="navbar" :class="{ 'active': isMenuOpen }" class="d-flex align-items-center justify-content-center m-0">
      <a href="https://www.facebook.com" target="_blank"><font-awesome-icon :icon="['fab', 'square-facebook']" /> Facebook</a>
      <a href="https://www.twitter.com" target="_blank"><font-awesome-icon :icon="['fab', 'twitter']" /> Twitter</a>
      <a href="https://www.instagram.com" target="_blank"><font-awesome-icon :icon="['fab', 'square-instagram']" /> Instagram</a>
    </nav>
    <div id="mobile" class="d-none align-items-center">
      <a><i class="bi bi-bag bag"></i></a>
      <span class="d-flex align-items-end" @click="toggleMenu">
        <i  class="fas fa-outdent bar"></i>
      </span>
    </div>
  </section>

  <section id="header" :class="{'bg-transparent': isTransparent}" class="d-flex align-items-center justify-content-between position-sticky top-0 left-0">
    <div class="webName">JMYmelody</div>
    <!-- <img src="../../img/logo.png" class="logo" alt=""> -->

    <nav id="navbar" :class="{ 'active': isMenuOpen }" class="d-flex align-items-center justify-content-center m-0">
      <RouterLink active-class="active" to="/">Home</RouterLink>
      <RouterLink active-class="active" to="/shop">Shop</RouterLink>
      <RouterLink active-class="active" to="/blog">Blog</RouterLink>
      <RouterLink active-class="active" to="/about">About</RouterLink>
      <RouterLink active-class="active" to="/contact">Contact</RouterLink>
      <RouterLink active-class="active" to="/login">login</RouterLink>
      <RouterLink active-class="active" to="/cart"><i  id="lg-bag" class="bi bi-bag"></i></RouterLink>
      <RouterLink active-class="active" to="/" href="#" id="close" @click="toggleMenu"><i class="bi bi-x-lg"></i></RouterLink>
    </nav>
    <div id="mobile" class="d-none align-items-center">
      <a><i class="bi bi-bag bag"></i></a>
      <span class="d-flex align-items-end" @click="toggleMenu">
        <i  class="fas fa-outdent bar"></i>
      </span>
    </div>
  </section>

  <transition name="slide">
    <div class="top-scroll" v-if="showScrollTop" @click="scrollToTop">
      <a class="btn-floating" href="#!">
        <font-awesome-icon :icon="['fas', 'arrow-up']" style="color: #fff; font-size: 28px;" />
      </a>
    </div>
  </transition>

</template>

<style src="../../assets/header.sass" scoped lang="sass"></style>

文章作者: Alex Lee
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Alex Lee !
评论
  目录