Bootstrap

【Springboot3+vue3】从零到一搭建Springboot3+vue3前后端分离项目之前端环境搭建

主要参考的博客为:

从零搭建SpringBoot3+Vue3前后端分离项目基座,中小项目可用_springboot+vue3-CSDN博客

记录一下自己的实现过程。

最终实现效果如下:

在这里插入图片描述

后端环境搭建参考博客【Springboot3+vue3】从零到一搭建Springboot3+vue3前后端分离项目之后端环境搭建

2 前端环境搭建

2.1 环境准备

  • node安装
  • vscode安装

2.2 创建Vue3项目

在将要存放vue3项目的路径打开cmd,使用以下命令创建项目

npm init vue@latest

在这里插入图片描述

此时项目创建完成,vscode打开项目目录,在资源目录空白右键,打开终端

在这里插入图片描述

执行命令 cnpm install安装依赖,等待安装完成后执行 cnpm run dev 运行项目

在这里插入图片描述

访问路径http://localhost:5173可访问项目

在这里插入图片描述

在终端ctrl c 可停止运行项目
项目描述如图

在这里插入图片描述

2.3 项目搭建准备

项目中使用组合式API

删除components下的所有文件,将App.vue文件内容修改为如下

<script setup>

</script>

<template>
    <router-view></router-view>
</template>

<style scoped>

</style>

2.4 安装Element Plus

  • 安装 cnpm install element-plus --save

  • cnpm install @element-plus/icons-vue

    在这里插入图片描述

2.5 安装axios

  • cnpm install axios

2.5.1 配置(创建实例,配置请求,响应拦截器)

在src目录下新建utils,并在utils下创建request.js进行axios配置

在这里插入图片描述

src/utils/request.js

// 请求配置

import axios from "axios";

// 定义公共前缀,创建请求实例
// const baseUrl = "http://localhost:8080";
const baseURL = '/api/';
const instance = axios.create({baseURL})


import { ElMessage } from "element-plus"
import { useTokenStore } from "@/stores/token.js"
// 配置请求拦截器
instance.interceptors.request.use(
    (config) => {
        // 请求前回调
        // 添加token
        const tokenStore = useTokenStore()
        // 判断有无token
        if (tokenStore.token) {
            config.headers.Authorization = tokenStore.token
        }
        return config
    },
    (err) => {
        // 请求错误的回调
        Promise.reject(err)
    }
)


import router from "@/router";
// 添加响应拦截器
instance.interceptors.response.use(
    result => {
        // 判断业务状态码
        if (result.data.code === 1) {
            return result.data;
        }
        // 操作失败
        ElMessage.error(result.data.message ? result.data.message : '服务异常')
        // 异步操作的状态转换为失败
        return Promise.reject(result.data)
    },
    err => {
        // 判断响应状态码, 401为未登录,提示登录并跳转到登录页面
        if (err.response.status === 401) {
            ElMessage.error('请先登录')
            router.push('/login')
        } else {
            ElMessage.error('服务异常')
        }
        // 异步操作的状态转换为失败
        return Promise.reject(err)  
    }
)

export default instance

2.5.2 配置跨域

在vite.config.js中加入如下内容

在这里插入图片描述

  server: {
    proxy: {
        '/api': {   // 获取路径中包含了/api的请求
            target: 'http://localhost:9999',        // 服务端地址
            changeOrigin: true, // 修改源
            rewrite:(path) => path.replace(/^\/api/, '')   // api 替换为 ''
        }
    }
  }

2.6 Vue Router安装使用

  • 安装 cnpm install vue-router@4

  • 在src/router/index.js中创建路由器并导出。index.js文件内容如下

    // 导入vue-router
    import {createRouter, createWebHistory} from 'vue-router'
    
    // 导入组件
    import LoginVue from '@/views/Login.vue'
    import LayoutVue from '@/views/Layout.vue'
    import UserList from '@/views/user/UserList.vue'
    import EditPassword from '@/views/user/EditPassword.vue'
    import DisplayUser from '@/views/user/DisplayUser.vue'
    
    // 定义路由关系
    const routes = [
        {path: '/login', component: LoginVue},
        {
            path: '/', component: LayoutVue, redirect: '', children: [
                {path: '/user/userlist', name: "/user/userlist", component: UserList, meta: {
                    title: "用户列表"
                  },},
                {path: '/user/editpassword', name: "/user/editpassword", component: EditPassword, meta: {
                    title: "修改密码"
                }
                },
                {path: '/user/displayuser', name: "/user/displayuser", component: DisplayUser, meta: {
                    title: "个人信息"
                }}
            ]
        }
    ]
    
    // 创建路由器 
    const router = createRouter({
        history: createWebHistory(),
        routes: routes
    })
    
    export default router
    
  • 在vue实例中使用vue-router,修改main.js文件内容为如下

import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from '@/router'
import { createPinia } from 'pinia'
const pinia = createPinia()

import zhLocale from 'element-plus/es/locale/lang/zh-cn'

createApp(App).use(router).use(ElementPlus, {locale: zhLocale}).use(pinia).mount('#app')

  • 在app.vue中声明router-view标签,展示组件内容。app.vue文件内容如下

    
    <script setup>
    
    </script>
    
    <template>
        <router-view></router-view>
    </template>
    
    <style scoped>
    
    </style>
    

2.7 Pinia状态管理库

  • 安装 cnpm install pinia

  • 安装persist cnpm install pinia-persistedstate-plugin

  • main.js中使用persist

    import { createPersistedState } from 'pinia-persistedstate-plugin'
    const persist = createPersistedState()
    pinia.use(persist)
    

    main.js内容整体如下:

    import './assets/main.css'
    
    import { createApp } from 'vue'
    import App from './App.vue'
    import ElementPlus from 'element-plus'
    import 'element-plus/dist/index.css'
    import router from '@/router'
    import { createPinia } from 'pinia'
    const pinia = createPinia()
    import { createPersistedState } from 'pinia-persistedstate-plugin'
    const persist = createPersistedState()
    pinia.use(persist)
    import zhLocale from 'element-plus/es/locale/lang/zh-cn'
    
    createApp(App).use(router).use(ElementPlus, {locale: zhLocale}).use(pinia).mount('#app')
    
    
  • src/stores/下定义token.js和userInfo.js来存储token和用户相关信息

token.js


// 定义 store
import { defineStore } from "pinia"
import {ref} from 'vue'
/*
    第一个参数:名字,唯一性
    第二个参数:函数,函数的内部可以定义状态的所有内容

    返回值: 函数
 */
export const useTokenStore = defineStore('token', () => {
    // 响应式变量
    const token = ref('')

    // 修改token值函数
    const setToken = (newToken) => {
        token.value = newToken
    }

    // 移除token值函数
    const removeToke = () => {
        token.value = ''
    }

    return {
        token, setToken, removeToke
    }
}, 
{
    persist: true   // 持久化存储
}
)

userInfo.js


import { defineStore } from "pinia"
import {ref} from 'vue'

const useUserInfoStore = defineStore('userInfo', () => {
    const info = ref({})
    
    const setInfo = (newInfo) => {
        info.value = newInfo
    }

    const removeInfo = () => {
        info.value = {}
    }

    return {info, setInfo, removeInfo}
},
{
    persist: true
}
)

export default useUserInfoStore;

2.8 搭建管理页面基础框架

2.8.1 在src/api/下创建user.js,封装请求方法

在这里插入图片描述

import request from "@/utils/request.js"

// 登录接口调用函数
export const userLoginService = (loginData) => {
    return request.post('/user/login', loginData)
}

// 获取当前登录用户信息
export const currentUserService = () => {
    return request.get('/user/currentUser')
}

// 获取所有用户信息
export const allUserService = () => {
    return request.get('/user/userList')
}

// 分页查询
export const pageListService = (pageParam) => {
    return request.get('/user/pageList', {params: pageParam})
}

// 新增用户
export const addUserService = (addData) => {
    return request.post('/user/add', addData)
}

// 根据id获取用户信息
export const getUserById = (id) => {
    return request.get('/user/getuserById', {params: id})
}

// 修改用户信息
export const updateUserService = (data) => {
    return request.put('/user/update', data)
}

// 删除用户
export const deleteByIdService = (id) => {
    console.log("deleteRequestid:", id)
    return request.delete('/user/delete/' + id)
}

2.8.2 登陆页面

  • 安装 cnpm install sass -D

在src下创建vuew项目,用于存放vue页面组件

在这里插入图片描述

Header.vue

<template>
    <div class="container">
        <!-- div left -->
        <div class="left">
            <!-- 折叠按钮-->
            <div @click="toggleCollapse()">
                <el-icon size="24" v-show="!isMenuOpen"><Fold /></el-icon>
                <el-icon size="24" v-show="isMenuOpen"><Expand /></el-icon>
            </div>
            <!-- 面包屑 -->
            <div>
                <el-breadcrumb separator="/">
                    <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
                    <template v-for="(item, index) in breadList">
                    <el-breadcrumb-item
                        v-if="item.name"
                        :key="index"
                        :to="item.path"
                    >{{ item.meta.title }}</el-breadcrumb-item>
                    </template>
                </el-breadcrumb>
            </div>
        </div>
        <!-- div right -->
        <div class="right">
            <div>
                <span >账号:{{userData.loginName}}</span>
            </div>
            <div>
                <el-avatar> {{userData.name}} </el-avatar>
            </div>
            <div>
                <el-dropdown>
                <el-icon size="24"><MoreFilled /></el-icon>
                <template #dropdown>
                <el-dropdown-menu>
                    <el-dropdown-item>
                        <el-icon><UserFilled /></el-icon>个人信息
                    </el-dropdown-item>
                    <el-dropdown-item>
                        <el-icon><EditPen /></el-icon>修改密码
                    </el-dropdown-item>
                    <el-dropdown-item>
                        <el-icon><ArrowLeft /></el-icon>退出登录
                    </el-dropdown-item>
                </el-dropdown-menu>
                </template>
            </el-dropdown>
            </div>
        </div>
    </div>
</template>

<script setup>
    import {
        Fold,
        Expand,
        MoreFilled,
        EditPen,
        ArrowLeft,
        UserFilled
    } from '@element-plus/icons-vue'
    import {ref, defineEmits, watch} from 'vue'

    // 面包屑
    import { useRouter,useRoute } from 'vue-router'

    let router = useRouter()
    let route = useRoute()

    let breadList = ref()
    let getMatched=()=>{
        console.log("route.matched:",route.matched);
        console
        breadList.value = route.matched.filter(item => item.meta && item.meta.title);
    }
    getMatched()
    
    watch(() => route.path, (newValue, oldValue) => { //监听路由路径是否发生变化,之后更改面包屑
        console.log("======")
        breadList.value = route.matched.filter(item => 
            item.meta && item.meta.title
    );
        console.log("breadList.value", breadList.value)
    })



    import useUserInfoStore from '@/stores/userinfo.js'
    const userInfoStore = useUserInfoStore();

    // 用户数据模型
    let userData = ref({
        id: '',
        name: '',
        loginName: ''
    })


    import {currentUserService} from '@/api/user.js'
    // 获取登录用户信息
    const getUser = async () => {
        let result = await currentUserService()
        // console.log(result)
        userData.value = result.data;
        userInfoStore.setInfo(result.data)
        // console.log("userData:",userData)
    }
    getUser()


    // 折叠按钮处理
    const emits = defineEmits(["parentClick"])
    const isMenuOpen = ref(false)
    const toggleCollapse = () => {
        isMenuOpen.value = !isMenuOpen.value
        console.log(isMenuOpen.value)
        emits("parentClick", isMenuOpen.value)
    }


</script>

<style lang="scss" scope>
    .container {  
        overflow: auto; /* 清除浮动影响 */  
        height: 48px;
        padding: 10px; /* 内边距 */  
        border-bottom: 2px solid; /* 设置下边框宽度和样式 */  
        border-bottom-color: #F5F5F5; /* 设置下边框颜色为红色 */  
        background-color: #FFFFFF;   
    }  
    
    .left {  
        height: 48px;
        float: left;   
        display: flex;
        align-items: center; /* 垂直居中子项 */  
        justify-content: center; /* 水平居中子项(如果需要)*/ 
    }  

    .left > div {  
        padding-right: 10px; /* 设置直接子元素的 padding */  
    }
    
    .right {  
        float: right; 
        display: flex; 
        align-items: center; /* 垂直居中子项 */  
        justify-content: center; /* 水平居中子项(如果需要)*/   
    }
    .right > div {  
        padding-right: 10px; /* 设置直接子元素的 padding */  
    }
</style>

Layout.vue

<script setup>
import LeftLayout from './LeftLayout.vue'
import Header from './Header.vue'
import MainView from './MainView.vue'

import {ref} from 'vue'
const isCollapse = ref(false)
const parentClick = (isCollapseValue) => {
    isCollapse.value = isCollapseValue;
    console.log(isCollapse.value)
}
</script>


<template>
  <div class="common-layout">
    <el-container>
      <LeftLayout :isCollapse='isCollapse' />
      <el-container>
        <el-header style="padding: 0"><Header @parentClick='parentClick'/></el-header>
        
        <el-main style="padding: 16px 8px 6px 8px"><MainView/></el-main>
        <el-footer>后台 ©2024 Created by buzhisuoyun</el-footer>
      </el-container>
    </el-container>
  </div>
</template>

<style scoped>
    .el-footer {
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 14px;
        color: #666;
        height: 38px;
        padding: 0;
        background-color: #FFFFFF; 
    }
</style>

LeftLayout.vue

<template>
    <el-row class="tac">
      <el-col >
        <el-menu
          default-active="2"
          class="el-menu-vertical-demo"
          :collapse="isCollapse"
          :router="true"
        >
          <!-- 标题 -->
          <div class="containerdiv">  
              <img src="../assets/favicon.ico" alt="Your Image" class="image">  
              <span class="text">后台管理</span>  
          </div>
  
          <!-- 菜单 -->
          <el-sub-menu index="1">
            <template #title>
              <el-icon><Share /></el-icon>
              <span>API管理</span>
            </template>
              <el-menu-item index="/api/apilist">API列表</el-menu-item>
              <el-menu-item index="1-2">item two</el-menu-item>
              <el-menu-item index="1-3">item three</el-menu-item>
            <el-sub-menu index="1-4">
              <template #title>item four</template>
              <el-menu-item index="1-4-1">item one</el-menu-item>
            </el-sub-menu>
          </el-sub-menu>
          <el-menu-item index="2">
            <el-icon><icon-menu /></el-icon>
            <span>Navigator Two</span>
          </el-menu-item>
          <el-menu-item index="3" disabled>
            <el-icon><document /></el-icon>
            <span>Navigator Three</span>
          </el-menu-item>
          <el-menu-item index="4">
            <el-icon><setting /></el-icon>
            <span>Navigator Four</span>
          </el-menu-item>
          <el-sub-menu index="5">
            <template #title>
              <el-icon><UserFilled /></el-icon>
              <span>用户管理</span>
            </template>
              <el-menu-item index="/user/userlist">用户列表</el-menu-item>
              <el-menu-item index="/user/displayuser">个人信息</el-menu-item>
              <el-menu-item index="/user/editpassword">修改密码</el-menu-item>
          </el-sub-menu>
        </el-menu>
      </el-col>
    </el-row>
  </template>
  
  <script lang="ts" setup>
  import {
    Document,
    Menu as IconMenu,
    Location,
    Share,
    UserFilled,
    Setting,
  } from '@element-plus/icons-vue'
  import {ref, defineProps} from 'vue'
  
  type Props = {
      isCollapse: boolean
  }
  defineProps<Props>()
  
  </script>
  
  <style scoped>
      .el-menu-vertical-demo {
          height: 100vh;
      }
      .el-menu-item {
          min-width: 0;
      }
      .containerdiv {  
          /* 你可以设置容器的样式,例如宽度、高度、背景色等 */  
          /* width: 300px; /* 示例宽度 */  
          height: 48px;  
          
          padding: 10px; /* 内边距 */  
          border-bottom: 2px solid; /* 设置下边框宽度和样式 */  
          border-bottom-color: #F5F5F5; /* 设置下边框颜色为红色 */  
      }  
    
      .image {  
          display: inline-block;  
          vertical-align: middle; /* 图片与文字垂直居中对齐 */  
          margin-right: 6px; /* 图片右边距,可选 */  
          width: 20px;
      }  
      
      .text {  
          display: inline-block;  
          vertical-align: middle; /* 文字与图片垂直居中对齐 */  
          font-weight: bold; /* 加粗文字 */  
          font-size: 14px;
      }
  </style>
  

Login.vue

<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
//定义数据模型
const registerData = ref({
    loginName: 'admin',
    password:'admin',
    rePassword: ''
})

// 定义表单组件的引用
const ruleFormRef = ref(null)

//定义表单校验规则
const rules = ref({
    loginName: [
        { required: true, message: '请输入用户名', trigger: 'blur' },
        { min: 5, max: 16, message: '长度为5~16位非空字符', trigger: 'blur' }
    ],
    password: [
        { required: true, message: '请输入密码', trigger: 'blur' },
        { min: 5, max: 16, 2: '长度为5~16位非空字符', trigger: 'blur' }
    ]
})

//绑定数据,复用注册表单的数据模型
//表单数据校验
//登录函数
import {userLoginService} from '@/api/user.js'
import {useTokenStore} from '@/stores/token.js'
import {useRouter} from 'vue-router'
const router = useRouter()
const tokenStore = useTokenStore();
const login = async ()=>{
    // 校验表单
    if (!ruleFormRef.value) return
    console.log("校验")
    await ruleFormRef.value.validate(async (valid) => {
        if (valid) {
            console.log("校验成功")
            // 调用接口,完成登录
            let result = await userLoginService(registerData.value);
            /* if(result.code===0){
                alert(result.msg? result.msg : '登录成功')
            }else{
                alert('登录失败')
            } */
            //alert(result.msg? result.msg : '登录成功')
            // ElMessage.success(result.msg ? result.msg : '登录成功')
            ElMessage.success(result.msg ? '登录成功': result.msg) //提示信息
            //token存储到pinia中
            tokenStore.setToken(result.data)
            //跳转到首页 路由完成跳转
            router.push('/')
        } else {
            console.log("校验失败")
        }
    })
}

//定义函数,清空数据模型的数据
const clearRegisterData = ()=>{
    registerData.value={
        loginName: '',
        password:'',
        rePassword:''
    }
}
</script>

<template>
    <el-row class="login-page">
        <el-col :span="12" class="bg"></el-col>
        <el-col :span="6" :offset="3" class="form">
            <!-- 登录表单 -->
            <el-form ref="ruleFormRef" :model=registerData size="large" autocomplete="off" :rules="rules">
                <el-form-item>
                    <h1>登录</h1>
                </el-form-item>
                <el-form-item prop="loginName">
                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.loginName"></el-input>
                </el-form-item>
                <el-form-item prop="password">
                    <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>
                </el-form-item>
                <el-form-item class="flex">
                    <div class="flex">
                        <el-checkbox>记住我</el-checkbox>
                        <!-- <el-link type="primary" :underline="false">忘记密码?</el-link> -->
                    </div>
                </el-form-item>
                <!-- 登录按钮 -->
                <el-form-item>
                    <el-button class="button" type="primary" auto-insert-space @click="login">登录</el-button>
                </el-form-item>
            </el-form>
        </el-col>
    </el-row>
</template>

<style lang="scss" scoped>
/* 样式 */
.login-page {
    height: 100vh;
    background-color: #fff;

    .bg {
        background: url('@/assets/login_bg.jpg') no-repeat center / cover;
        border-radius: 0 20px 20px 0;
    }

    .form {
        display: flex;
        flex-direction: column;
        justify-content: center;
        user-select: none;

        .title {
            margin: 0 auto;
        }

        .button {
            width: 100%;
        }

        .flex {
            width: 100%;
            display: flex;
            justify-content: space-between;
        }
    }
}
</style>

MainView.vue

<template>
	<div class="app-main">
		<!-- <transition name="fade-transfrom" mode="out-in">
			<router-view></router-view>
		</transition> -->
        <router-view v-slot="{ Component }">
            <transition>
                <component :is="Component" />
            </transition>
        </router-view>
	</div>
</template>

<style lang="scss" scope>
	.app-main{
		width:100%;
		height:100%;
        background-color: #FFFFFF; 
	}
</style>

user/DisplayUser.vue

<template>
    <div>
        <el-form :model="form" >
        <el-form-item label="账号" >
            <el-input v-model="form.loginName" :disabled="!isAdd"/>
        </el-form-item>
        <el-form-item label="姓名" >
            <el-input v-model="form.name" :disabled="!isAdd"/>
        </el-form-item>
        <el-form-item label="电话" >
            <el-input v-model="form.phone" />
        </el-form-item>
        <el-form-item label="性别">
            <el-radio-group v-model="form.sex" :disabled="!isAdd">
                <el-radio value="0" checked>女</el-radio>
                <el-radio value="1">男</el-radio>
            </el-radio-group>
    </el-form-item>
    </el-form>
      <div class="dialog-footer">
        <el-button @click="onDialogFormCancel">取消</el-button>
        <el-button type="primary" @click="onDialogFormConfirm">
            确认
        </el-button>
      </div>
    </div>
</template>
<script setup>
    import {ref} from 'vue'

    const form = ref({
        loginName: '',
        name: '',
        phone: '',
        sex: '0'
    })
    // 重置对话框表单
    const restForm = () => {
        form.value = {sex: '0'}
        title.value = '添加用户'
        isAdd.value = true
    }
    const isAdd = ref(true)


    // 提交事件
const onDialogFormConfirm = async () => {
    

}
// 取消事件
const onDialogFormCancel = () => {
   
}
</script>

EditPassword.vue

<template>
    <div> 修改密码</div>
</template>

UserList.vue

<script setup>
import { Plus } from "@element-plus/icons-vue";
import { ref, reactive } from "vue";
import { allUserService, pageListService, addUserService, getUserById, updateUserService, deleteByIdService } from "@/api/user.js";
import { ElMessage, ElMessageBox  } from "element-plus"

// 表单数据
const searchData = ref({
  name: "",
});
// 表格数据
const tableData = ref([]);

/** 分页 */
// 分页数据
const pageData = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 20,
})
// 分页插件,每页条数发生改变时
const handleSizeChange = (val) => {
  pageData.pageSize = val
  getPageList()
}
// 分页插件, 当页码发生改变时
const handleCurrentChange = (val) => {
  pageData.currentPage = val
  getPageList()
}

// // 查询所有用户
// const getAllUser = async () => {
//     const result = await allUserService()
//     tableData.value = result.data
// }
// getAllUser()

// 分页查询
const getPageList = async () => {
  const params = {
    currentPage: pageData.currentPage,
    pageSize: pageData.pageSize,
    name: searchData.value.name,
  }
  //console.log("params:", params);
  const result = await pageListService(params);
  pageData.total = result.data.total;
  tableData.value = result.data.items;
  //console.log("tableData:", tableData);
}
getPageList()

// 头部表单函数定义
const onSearch = () => {
    getPageList()
}
// 重置查询表单
const onRest = () => {
    searchData.value = {}
    getPageList()
}

/** 添加修改对话框表单 */
const form = ref({
  loginName: '',
  name: '',
  phone: '',
  sex: '0'
})
// 重置对话框表单
const restForm = () => {
    form.value = {sex: '0'}
    title.value = '添加用户'
    isAdd.value = true
}
const title = ref('添加用户')
const isAdd = ref(true)
const dialogFormVisible = ref(false)

// 提交对话框表单按钮事件
const onDialogFormConfirm = async () => {
    //1.验证表单
    if (!ruleFormRef.value) return
    //2.提交表单
    await ruleFormRef.value.validate((valid) => {
        if (valid) {    // 校验成功
            confirm()
        }
    })

}
// 取消对话表单框按钮事件
const onDialogFormCancel = () => {
    console.log("cancel......")
    dialogFormVisible.value = false
    restForm()
}

// 添加按钮事件
const onAdd = () => {
    // 打开对话框
    title.value = '添加用户'
    isAdd.value = true
    dialogFormVisible.value = true
}

// 修改按钮事件
const handleEdit = async (index, row) => {
    title.value = '修改用户'
    isAdd.value = false
    // 回显数据
    console.log("row:", row)
    const id = {id: row.id}
    let result = await getUserById(id)
    form.value = result.data
    // 控制只读属性
    dialogFormVisible.value = true
}

// 删除按钮事件
const handleDelete = (index, row) => {
    ElMessageBox.confirm(
    '确认要删除吗?',
    '提示',
    {
      confirmButtonText: '确认',
      cancelButtonText: '取消',
      type: 'warning',
    }
  )
    .then( async () => {
      // 删除
      console.log("delete=====")

        let result = await deleteByIdService(row.id)
        ElMessage.success(result.msg ? result.msg : '删除成功')
        getPageList()
    })
    .catch(() => {

    })
}

// 提交表单
const confirm = async () => {
    if(isAdd.value) {
        // 添加
        try {   // 添加成功
                let result = await addUserService(form.value)
                ElMessage.success(result.msg ? result.msg : '添加成功')
                // 关闭弹窗,清空表单
                dialogFormVisible.value = false
                restForm()
                getPageList()
            } catch (error) {
                
            }
    } else {
        console.log("update=======")
        //修改
        try {   // 修改成功
                let result = await updateUserService(form.value)
                ElMessage.success(result.msg ? result.msg : '修改成功')
                // 关闭弹窗,清空表单
                dialogFormVisible.value = false
                restForm()
                getPageList()
            } catch (error) {
                
            }
    }

}

/** 表单校验 */
const ruleFormRef = ref(null)   // 定义表单组件的引用
// 定义表单校验规则
const rules = ref({
    loginName: [
        { required: true, message: '请输入账号名', trigger: 'blur' }
    ],
    name: [
        { required: true, message: '请输入姓名', trigger: 'blur' }
    ],
    phone: [
        { required: true, trigger: 'blur',  message: "请输入正确手机号", validator: checkPhone }
    ]
})

// 手机号自定义校验
var checkPhone = (rule, value, callback) => {
    if (!value) {
        return callback(new Error('手机号不能为空'))
    } else {
        const reg = /^1[3|4|5|7|8][0-9]\d{8}$/
        console.log(reg.test(value))
        if (reg.test(value)) {
            callback()
        } else {
            return callback(new Error('请输入正确的手机号'))
        }
    }
}


</script>

<template>
  <div>
    <!-- 工具栏 -->
    <div>
      <el-row>
        <el-col :span="8">
          <!-- 操作按钮 -->
          <div class="operation-div">
            <el-button type="primary" @click="onAdd">添加</el-button>
          </div>
        </el-col>
        <el-col :span="16">
          <!-- 条件查询 -->
          <div class="search-div">
            <el-form :inline="true" :model="searchData" class="demo-form-inline">
              <el-form-item label="用户名:">
                <el-input
                  v-model="searchData.name"
                  placeholder="请输入用户名"
                  clearable
                />
              </el-form-item>
              <el-form-item>
                <el-button type="primary" @click="onSearch">查询</el-button>
                <el-button type="primary" @click="onRest">重置</el-button>
              </el-form-item>
            </el-form>
          </div>
        </el-col>
      </el-row>
    </div>
    <!-- 表格内容 -->
    <div>
      <el-table
        :data="tableData"
        border
        stripe
        style="width: 100%"
        :header-cell-style="{ background: '#ECF5FF' }"
      >
        <el-table-column type="index" :index="indexMethod" />
        <el-table-column prop="loginName" label="账号" />
        <el-table-column prop="name" label="姓名" />
        <el-table-column prop="phone" label="联系电话" />
        <el-table-column prop="sex" label="性别">
          <template #default="scope">
            <el-tag :type="scope.row.sex === '0'? '' : 'success'" disable-transitions>
              {{ scope.row.sex === '1' ? "男" : "女" }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作">
          <template #default="scope">
            <el-button size="small" @click="handleEdit(scope.$index, scope.row)"
              >编辑</el-button
            >
            <el-button
              size="small"
              type="danger"
              @click="handleDelete(scope.$index, scope.row)"
              >删除</el-button
            >
          </template>
        </el-table-column>
      </el-table>
      <!-- 分页 -->
      <div style="margin-top: 20px">
        <el-pagination
          v-model:current-page="pageData.currentPage"
          v-model:page-size="pageData.pageSize"
          :page-sizes="[10, 20, 50, 100]"
          
          background
          layout="->, jumper, total, sizes, prev, pager, next"
          :total="pageData.total"
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
        />
      </div>
    </div>
  </div>

  <!-- 添加修改对话框表单-->
  <el-dialog v-model="dialogFormVisible" :title="title" width="500" draggable overflow @close='onDialogFormCancel'>
    <el-form :model="form" ref="ruleFormRef" :rules="rules">
        <el-form-item label="账号" prop="loginName">
            <el-input v-model="form.loginName" :disabled="!isAdd"/>
        </el-form-item>
        <el-form-item label="姓名" prop="name">
            <el-input v-model="form.name" :disabled="!isAdd"/>
        </el-form-item>
        <el-form-item label="电话" prop="phone">
            <el-input v-model="form.phone" />
        </el-form-item>
        <el-form-item label="性别">
            <el-radio-group v-model="form.sex" :disabled="!isAdd">
                <el-radio value="0" checked>女</el-radio>
                <el-radio value="1">男</el-radio>
            </el-radio-group>
    </el-form-item>
    </el-form>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="onDialogFormCancel">取消</el-button>
        <el-button type="primary" @click="onDialogFormConfirm">
            确认
        </el-button>
      </div>
    </template>
  </el-dialog>
</template>



<style scoped>
.operation-div {
  width: 100%;
  text-align: left;
  padding-left: 10px;
  padding-top: 10px;
}

.search-div {
  width: 100%;
  text-align: right;
  padding-top: 10px;
}
</style>

2.9 运行展示

2.9.1 启动前端

  • cnpm run dev

在这里插入图片描述

进入http://localhost:5173

2.9.2 启动后端

运行后端项目

2.9.3 测试

登录界面,使用之前swagger测试时添加的用户登录即可。

在这里插入图片描述

在这里插入图片描述

;