Vue工程化+Tialis案例

Vue工程化快速入门


1.环境搭建

在官网下载nodejs安装包,安装在终端输入命令查看是否查看成功

node -v
npm -v

配置npm软件包的默认位置

npm config set prefix [node install 目录]

如果安装速度较慢,可以配置淘宝镜像

npm config set registry https://registry.npmmirror.com

2. Vue工程创建

1.创建Vue工程

npm create vue@3.3.4

执行上述命令后,先手需要输入项目的名称,接着选择项目的一些依赖,初期只需要全选择No即可

vue工程创建后目录结构

文件/目录 含义
.vscode 这是一个隐藏目录,用于存储与 Visual Studio Code 相关的配置文件,如工作区设置、调试配置等。如果你不使用 VSCode,这个目录可以忽略或删除。
node_modules 这个目录包含了项目依赖的所有 npm 包。当你运行 npm installyarn install 时,所有的依赖包都会被下载并安装到这个目录中。通常这个目录会被添加到 .gitignore 文件中,以避免提交到版本控制系统。
public 这个目录包含静态资源文件,如图片、字体等。这些文件会直接复制到构建输出目录中,并且可以通过相对路径访问。例如,public/favicon.ico 可以通过 /favicon.ico 访问。
src 这是项目的源代码目录,包含了所有的 Vue 组件、样式、脚本等开发文件。这是你主要编写和修改代码的地方。
assets 存放项目中的静态资源文件,如图片、图标、音频等。这些文件在构建过程中会被处理并引用到相应的组件中。
components 存放 Vue 组件的目录。每个组件通常是一个单独的 .vue 文件,包含了模板、逻辑和样式。
App.vue 这是项目的根组件,所有的其他组件都是它的子组件。它通常定义了应用的基本结构和布局。
main.js 这是项目的入口文件,负责启动 Vue 应用。在这个文件中,你会创建 Vue 实例,并挂载到 index.html 中的某个元素上。
.gitignore 这个文件定义了 Git 版本控制系统应该忽略的文件和目录模式。它帮助你避免将不需要跟踪的文件(如编译输出、临时文件等)提交到仓库中。
index.html 这是项目的入口 HTML 文件。Vue 应用会被挂载到这个文件中的某个元素上(通常是 <div id="app"></div>)。你可以在这里进行一些基本的 HTML 结构定义和全局样式设置。
package-lock.json 这个文件记录了项目依赖的具体版本信息,确保在不同环境中安装依赖时能够保持一致性。它是 npm 自动生成的,不应该手动编辑。
package.json 这个文件定义了项目的元数据和配置信息,包括项目名称、版本、描述、作者、许可证、依赖项、脚本命令等。它是每个 Node.js 项目的必备文件。
README.md 这是一个可选的文档文件,通常用于提供项目的介绍、安装指南、使用说明等信息。Markdown 格式使得它可以被很好地渲染为网页格式。
vite.config.js 这个文件是 Vite 构建工具的配置文件。Vite 是一个现代前端构建工具,特别适合快速启动和开发 Vue 项目。在这个文件中,你可以配置 Vite 的各种选项,如插件、服务器设置、构建输出等。

2.安装依赖并启动Vue工程

构建好vue工程后,接着会提示下方代码:

 Junglezt: vue ❯ npm create vue@3.3.4

> npx
> create-vue


Vue.js - The Progressive JavaScript Framework

√ Project name: ... vue-project1
√ Add TypeScript? ... No / Yes
√ Add JSX Support? ... No / Yes
√ Add Vue Router for Single Page Application development? ... No / Yes
√ Add Pinia for state management? ... No / Yes
√ Add Vitest for Unit Testing? ... No / Yes
√ Add Cypress for both Unit and End-to-End testing? ... No / Yes
√ Add ESLint for code quality? ... No / Yes

Scaffolding project in E:\source\vue\vue-project1...

Done. Now run:

  cd vue-project1
  npm install
  npm run dev

提示执行cd vue-project1进入创建好的vue工程目录中,执行npm install安装工程依赖的包

npm install 是根据当前项目(也就是当前目录)中的packge.json文件确定需要下载的依赖

最后是npm run dev,执行后就会启动项目,如下:

  VITE v6.3.5  ready in 444 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

根据提示使用浏览器访问http://localhost:5173端口即可访问vue工程的初始样式。

3.组件关系

  1. 在使用浏览器访问主页的时候,默认会访问index.html
  2. index.html中,通常使用<script src="/src/main.js</script>引用了main.js文件并且<div id="app">绑定了app
  3. main.js中通过vue导入了createApp方法,通过createApp(App).mount(#app)App.vue组件挂载到#app锚点
  4. App.vue为根组件,包含script、template、style三个区域,分别表示js、html、css

3.API风格

  • Vue有两种不同的API风格:选项式API组合式API

根据官方的推荐,如果是单独或者小型的网页开发,推荐使用选项式API,如果是较为大型,推荐使用工程化+选项式API

一下是两种不同风格API的语法结构:

选项式API

<script>
export default {
  data() {
    return {
      message: '你好,Vue!'
    }
  },
  methods: {
    reverseMessage() {
      this.message = this.message.split('').reverse().join('');
    }
  }
}
</script>

<template>
  <div>
    <p>{{ message }}</p>
    <button @click="reverseMessage">反转消息</button>
  </div>
</template>

组合式API

<script>
import { ref } from 'vue';

export default {
  setup() {
    const message = ref('你好,Vue!');

    const reverseMessage = () => {
      message.value = message.value.split('').reverse().join('');
    };

    return {
      message,
      reverseMessage
    };
  }
}
</script>

<template>
  <div>
    <p>{{ message }}</p>
    <button @click="reverseMessage">反转消息</button>
  </div>
</template>

注:上述中在script标签中并没有使用setup标识,所以需要使用export default设置一个默认导出

在使用<script setup>时,可以不使用export default,因为Vue已经为你处理好了这一切。这使得组件开发变得更简单、更直接,特别适合喜欢函数式编程风格或希望减少代码复杂度的开发者。

Element Plus

Element 是饿了么团队开发,基于Vue3,面向设计师和开发者的组件库。

组件:组成网页的部件,例如: 超链接、按钮、图片、表格、表单、分页条等等。

1.环境搭建

1.安装element-plus依赖

参考官方文档: https://cn.element-plus.org/zh-CN/guide/installation.html

npm install element-plus@2.4.4 --save
# npm install element-plus --save

# --save 参数表示将element-plus添加到packge.json中

2.在main.js引入element-plus

import { createApp } from 'vue'

// 引入element-plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'


createApp(App).use(ElementPlus).mount('#app')

3.在Vue组件中使用element-plus

src/views/ElementDemo.vue

<script setup>

</script>

<template>
    <div class="mb-4">
    <el-button>Default</el-button>
    <el-button type="primary">Primary</el-button>
    <el-button type="success">Success</el-button>
    <el-button type="info">Info</el-button>
    <el-button type="warning">Warning</el-button>
    <el-button type="danger">Danger</el-button>
  </div>

  <div class="mb-4">
    <el-button plain>Plain</el-button>
    <el-button type="primary" plain>Primary</el-button>
    <el-button type="success" plain>Success</el-button>
    <el-button type="info" plain>Info</el-button>
    <el-button type="warning" plain>Warning</el-button>
    <el-button type="danger" plain>Danger</el-button>
  </div>
</template>

<style scoped>

</style>

App.vue中导入./views/ElementDemo.vue

<script setup>
import ElementDemo from './views/ElementDemo.vue';

</script>

<template>
<ElementDemo></ElementDemo>
</template>

<style scoped>

</style>

2.使用element-plus

element-plus也叫做element-ui,专注提供vue2服务,element-plus专注于vue3

ElementDemom.vue导入组件测试

  <script setup>
  import { ref } from 'vue'

  // 表格数据
  const tableData = [
    { date: '2016-05-03', name: 'Tom1', address: 'No. 189, Grove St, Los Angeles', },
    { date: '2016-05-02', name: 'Tom2', address: 'No. 189, Grove St, Los Angeles', },
    { date: '2016-05-04', name: 'Tom3', address: 'No. 189, Grove St, Los Angeles', },
    { date: '2016-05-01', name: 'Tom4', address: 'No. 189, Grove St, Los Angeles', },
  ]


  // 分页数据
  const currentPage4 = ref(1)
  const pageSize4 = ref(100)
  const background = ref(true)
  const disabled = ref(false)

  const handleSizeChange = (val) => {
    console.log(`当前分页: ${val}`)
  }
  const handleCurrentChange = (val) => {
    console.log(`当前页: ${val}`)
  }



  // Dialog对话框
  const dialogTableVisible = ref(false)   // 控制Dialog对话框的显示与隐藏







  // 表单数据
  const user = ref({
    name: '',
    gender: '',
    birthday: '',
  })

  const onSubmit = () => {
    console.log(user.value)
  }
</script>



  <template>

    <!-- 按钮 -->
    <div class="mb-4">
      <el-button>Default</el-button>
      <el-button type="primary">Primary</el-button>
      <el-button type="success">Success</el-button>
      <el-button type="info">Info</el-button>
      <el-button type="warning">Warning</el-button>
      <el-button type="danger">Danger</el-button>
    </div>
    <div class="mb-4">
      <el-button plain>Plain</el-button>
      <el-button type="primary" plain>Primary</el-button>
      <el-button type="success" plain>Success</el-button>
      <el-button type="info" plain>Info</el-button>
      <el-button type="warning" plain>Warning</el-button>
      <el-button type="danger" plain>Danger</el-button>
    </div>

    <!-- 表格 -->
    <el-table :data="tableData" stripe style="width: 100%">
      <el-table-column prop="name" label="姓名" width="180" />
      <el-table-column prop="date" label="生日" width="180" />
      <el-table-column prop="address" label="地址" />
    </el-table>


    <!-- 分页 -->
    <div class="demo-pagination-block">
      <div class="demonstration">All combined</div>
      <el-pagination v-model:current-page="currentPage4" v-model:page-size="pageSize4"
        :page-sizes="[100, 200, 300, 400]" :disabled="disabled" :background="background"
        layout="total, sizes, prev, pager, next, jumper" :total="1000" @size-change="handleSizeChange"
        @current-change="handleCurrentChange" />
    </div>


    <!-- Dialog对话框 -->
    <el-button plain @click="dialogTableVisible = true">
      这是一个Dialog对话框
    </el-button>
    <el-dialog v-model="dialogTableVisible" title="购物信息" width="800">
      <el-table :data="tableData">
        <el-table-column property="date" label="时间" width="150" />
        <el-table-column property="name" label="姓名" width="200" />
        <el-table-column property="address" label="地址" />
      </el-table>
    </el-dialog>


    <!-- 表单 -->
    <el-form :inline="true" :model="user" class="demo-form-inline">
      <el-form-item label="用户名">
        <el-input v-model="user.name" placeholder="请输入用户名" clearable />
      </el-form-item>
      <el-form-item label="性别">
        <el-select v-model="user.gender" placeholder="请选择" clearable>
          <el-option label="男" value="1" />
          <el-option label="女" value="2" />
        </el-select>
      </el-form-item>
      <el-form-item label="生日">
        <el-date-picker v-model="user.birthday" type="date" placeholder="请选择" clearable value-format="YYYY-MM-DD" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSubmit">提交</el-button>
      </el-form-item>
    </el-form>


    <hr>
    <br>
    <br>
    <br>
    <br>
    <br>
    <br>
    <br>
    <br>


    <!-- Container布局组件 -->
    <div class="common-layout">
      <el-container>
        <el-header><el-form :inline="true" :model="user" class="demo-form-inline">
            <el-form-item label="用户名">
              <el-input v-model="user.name" placeholder="请输入用户名" clearable />
            </el-form-item>
            <el-form-item label="性别">
              <el-select v-model="user.gender" placeholder="请选择" clearable>
                <el-option label="男" value="1" />
                <el-option label="女" value="2" />
              </el-select>
            </el-form-item>
            <el-form-item label="生日">
              <el-date-picker v-model="user.birthday" type="date" placeholder="请选择" clearable
                value-format="YYYY-MM-DD" />
            </el-form-item>
            <el-form-item>
              <el-button type="primary" @click="onSubmit">查询</el-button>
            </el-form-item>
          </el-form></el-header>
        <el-container>
          <el-aside width="200px">侧边栏</el-aside>
          <el-container>
            <el-main>
              <el-table :data="tableData" style="width: 100%">
                <el-table-column prop="date" label="Date" width="180" />
                <el-table-column prop="name" label="Name" width="180" />
                <el-table-column prop="address" label="Address" />
              </el-table>
            </el-main>
            <el-footer>
              <div class="demo-pagination-block">
                <div class="demonstration">All combined</div>
                <el-pagination v-model:current-page="currentPage4" v-model:page-size="pageSize4"
                  :page-sizes="[100, 200, 300, 400]" :disabled="disabled" :background="background"
                  layout="total, sizes, prev, pager, next, jumper" :total="400" @size-change="handleSizeChange"
                  @current-change="handleCurrentChange" />
              </div>
            </el-footer>
          </el-container>
        </el-container>
      </el-container>
    </div>



    <!-- layout布局 -->
    <el-row :gutter="20">
      <el-col :span="16">
        <div class="grid-content ep-bg-purple" />1
      </el-col>
      <el-col :span="8">
        <div class="grid-content ep-bg-purple" />2
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="8">
        <div class="grid-content ep-bg-purple" />3
      </el-col>
      <el-col :span="8">
        <div class="grid-content ep-bg-purple" />
        <el-button>Default</el-button>
      </el-col>
      <el-col :span="4">
        <div class="grid-content ep-bg-purple" />5
      </el-col>
      <el-col :span="4">
        <div class="grid-content ep-bg-purple" />6
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="4">
        <div class="grid-content ep-bg-purple" />7
      </el-col>
      <el-col :span="16">
        <div class="grid-content ep-bg-purple" />8
      </el-col>
      <el-col :span="4">
        <div class="grid-content ep-bg-purple" />9
      </el-col>
    </el-row>

  </template>

<style scoped>
/* layout布局 */
.el-row {
  margin-bottom: 20px;
}

.el-row:last-child {
  margin-bottom: 0;
}

.el-col {
  border-radius: 4px;
}

.grid-content {
  border-radius: 4px;
  min-height: 36px;
}
</style>

更改App.vue根组件

<script setup>
import ElementDemo from './views/ElementDemo.vue';

</script>

<template>
<ElementDemo></ElementDemo>
</template>

<style scoped>

</style>

上述组件参考: https://cn.element-plus.org/zh-CN/component/overview.html

3.element-plus案例

员工列表查询,主要实现输入用户名、性别、职位,查询用户信息

EmpList.vue

<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'

const formInline = ref({
    name: '',
    gender: '',
    job: '',
})

onMounted(() => {
    search()
})

const search = async () => {
    const result = await axios.get(`https://web-server.itheima.net/emps/list?name=${formInline.value.name}&gender=${formInline.value.gender}&job=${formInline.value.job}`)

    tableData.value = result.data.data;
}

const clear = () => {
    formInline.value = { name: '', gender: '', job: '' };
    search()
}




const tableData = ref([
])
</script>

<template>
    <div id="container">
        <el-form :inline="true" :model="formInline" class="demo-form-inline">
            <el-form-item label="姓名">
                <el-input v-model="formInline.name" placeholder="请输入姓名" clearable />
            </el-form-item>
            <el-form-item label="性别">
                <el-select v-model="formInline.gender" placeholder="请选择" clearable>
                    <el-option label="男" value="1" />
                    <el-option label="女" value="2" />
                </el-select>
            </el-form-item>
            <el-form-item label="职位">
                <el-select v-model="formInline.job" placeholder="请选择" clearable>
                    <el-option label="班主任" value="1" />
                    <el-option label="学生主管" value="2" />
                    <el-option label="讲师" value="3" />
                    <el-option label="教研主管" value="4" />
                    <el-option label="咨询师" value="5" />
                </el-select>
            </el-form-item>

            <el-form-item>
                <el-button type="primary" @click="search">查询</el-button>
            </el-form-item>

            <el-form-item>
                <el-button type="primary" @click="clear">清空</el-button>
            </el-form-item>
        </el-form>




        <!-- 表格 -->
        <el-table :data="tableData" border style="width: 100%">
            <el-table-column prop="id" label="ID" align="center" />
            <el-table-column prop="name" label="姓名" align="center" />
            <el-table-column prop="image" label="头像" align="center">
                <template #default="scope">
                    <img v-bind:src="scope.row.image" height="40px"/>
                    <!-- 使用element-plus avatra组件 -->
                    <!-- <div class="demo-type">
                        <div>
                            <el-avatar v-bind:src="scope.row.image" />
                        </div>
                    </div> -->
                </template>
            </el-table-column>
            <el-table-column prop="gender" label="性别" align="center">
                <template #default="scope">
                    {{ scope.row.gender == 1 ? '男' : '女' }}
                </template>
            </el-table-column>
            <el-table-column prop="job" label="职位" align="center">
                <template #default="scope">
                    <sapn v-if="scope.row.job == 1">班主任</sapn>
                    <sapn v-if="scope.row.job == 2">学生主管</sapn>
                    <sapn v-if="scope.row.job == 3">讲师</sapn>
                    <sapn v-if="scope.row.job == 4">教研主管</sapn>
                    <sapn v-if="scope.row.job == 5">咨询师</sapn>
                </template>
            </el-table-column>
            <el-table-column prop="entrydate" label="入职时间" align="center" />
            <el-table-column prop="updatetime" label="更新时间" align="center" />
        </el-table>
    </div>
</template>

<style scoped>
#container {
    width: 75%;
    margin-left: 12.5%;
    margin-right: 12.5%;
}
</style>

App.vue

<script setup>
import EmpList from './views/EmpList.vue';

</script>

<template>
<EmpList></EmpList>
</template>

<style scoped>

</style>

Tlias案例

1.侧边栏(菜单)

找到element-plus官网侧边栏,修改即可,修改后如下

@/views/layout/index.vue

<script setup>

</script>

<template>
  <div class="common-layout">
    <el-container>
      <!-- Header 区域 -->
      <el-header class="header">
        <span class="title">Tlias智能学习辅助系统</span>
        <span class="right_tool">
          <a href="">
            <el-icon>
              <EditPen />
            </el-icon> 修改密码 &nbsp;&nbsp;&nbsp; | &nbsp;&nbsp;&nbsp;
          </a>
          <a href="">
            <el-icon>
              <SwitchButton />
            </el-icon> 退出登录
          </a>
        </span>
      </el-header>

      <el-container>
        <!-- 左侧菜单 -->
        <el-aside width="200px" class="aside">
          <el-scrollbar>
            <el-menu>
              <!-- 首页 -->
              <el-menu-item index="1">
                <el-icon><Promotion /></el-icon>
                <span>首页</span>
              </el-menu-item>

              <!-- 班级学员管理 -->
              <el-sub-menu index="2">
                <template #title>
                  <el-icon><Menu /></el-icon>
                  <span>班级学员管理</span>
                </template>
                <el-menu-item index="2-1">
                  <el-icon><HomeFilled /></el-icon>
                  <span>班级管理</span>
                </el-menu-item>
                <el-menu-item index="2-2">
                  <el-icon><UserFilled /></el-icon>
                  <span>学员管理</span>
                </el-menu-item>
              </el-sub-menu>

              <!-- 系统信息管理 -->
              <el-sub-menu index="3">
                <template #title>
                  <el-icon><Tools /></el-icon>
                  <span>系统信息管理</span>
                </template>
                <el-menu-item index="3-1">
                  <el-icon><HelpFilled /></el-icon>
                  <span>部门管理</span>
                </el-menu-item>
                <el-menu-item index="3-2">
                  <el-icon><Avatar /></el-icon>
                  <span>员工管理</span>
                </el-menu-item>
              </el-sub-menu>




              <!-- 数据统计管理 -->
              <el-sub-menu index="4">
                <template #title>
                  <el-icon><Histogram /></el-icon>
                  <span>数据统计管理</span>
                </template>
                <el-menu-item index="4-1">
                  <el-icon><InfoFilled /></el-icon>
                  <span>员工信息统计</span>
                </el-menu-item>
                <el-menu-item index="4-2">
                  <el-icon><Share /></el-icon>
                  <span>学员信息统计</span>
                </el-menu-item>
                <el-menu-item index="4-3">
                  <el-icon><Document /></el-icon> 
                  <span>日志信息统计</span>
                </el-menu-item>
              </el-sub-menu>




            </el-menu>
          </el-scrollbar>
        </el-aside>

        <el-main>
          右侧核心展示区域
        </el-main>
      </el-container>

    </el-container>
  </div>
</template>

<style scoped>
.header {
  background-image: linear-gradient(to right, #00547d, #007fa4, #00aaa0, #00d072, #a8eb12);
}

.title {
  color: white;
  font-size: 40px;
  font-family: 楷体;
  line-height: 60px;
  font-weight: bolder;
}

.right_tool {
  float: right;
  line-height: 60px;
}

a {
  color: white;
  text-decoration: none;
}

.aside {
  width: 220px;
  border-right: 1px solid #ccc;
  height: 730px;
}
</style>

2.动态菜单(核心展示区域)

需要使用vue-route;路由主要实现访问路径组件的对应关系

vue-route的组成:

  • Router实例:路由实例,基于createRouter函数创建,维护了应用的路由信息。
  • <router-link>:路由链接组件,浏览器会解析成<a>
  • <router-view>:动态视图组件,用来渲染展示与路由路径对应的组件。

一般使用需要在<template>标签中加入<router-link to="/index">包裹,to属性指定Router实例对应的组件;但在使用element-plus依赖中的Menu组件,父标签<el-menu>中含有一个参数为router="true",值为true则代表子标签中的index属性指定路由对应的组件,具体如下:

# 使用router-link
<router-link to="index">
    <el-menu-item index="1">
      <el-icon>
        <Promotion />
      </el-icon>
      <span>首页</span>
    </el-menu-item>
</router-link>

# 使用element-plus标签属性
<el-menu router>
  <!-- 首页 -->
    <el-menu-item index="index">
      <el-icon>
        <Promotion />
      </el-icon>
      <span>首页</span>
    </el-menu-item>
</el-menu>

上述两种方式效果一样。案例如下

修改@/views/layout/index.vueindexrouter属性

<script setup>

</script>

<template>
  <div class="common-layout">
    <el-container>
      <!-- Header 区域 -->
      <el-header class="header">
        <span class="title">Tlias智能学习辅助系统</span>
        <span class="right_tool">
          <a href="">
            <el-icon>
              <EditPen />
            </el-icon> 修改密码 &nbsp;&nbsp;&nbsp; | &nbsp;&nbsp;&nbsp;
          </a>
          <a href="">
            <el-icon>
              <SwitchButton />
            </el-icon> 退出登录
          </a>
        </span>
      </el-header>

      <el-container>
        <!-- 左侧菜单 -->
        <el-aside width="200px" class="aside">
          <el-scrollbar>
            <el-menu router>
              <!-- 首页 -->
                <el-menu-item index="/index">
                  <el-icon>
                    <Promotion />
                  </el-icon>
                  <span>首页</span>
                </el-menu-item>


              <!-- 班级学员管理 -->
              <el-sub-menu index="/manage">
                <template #title>
                  <el-icon>
                    <Menu />
                  </el-icon>
                  <span>班级学员管理</span>
                </template>
                <el-menu-item index="/clazz">
                  <el-icon>
                    <HomeFilled />
                  </el-icon>
                  <span>班级管理</span>
                </el-menu-item>
                <el-menu-item index="/stu">
                  <el-icon>
                    <UserFilled />
                  </el-icon>
                  <span>学员管理</span>
                </el-menu-item>
              </el-sub-menu>

              <!-- 系统信息管理 -->
              <el-sub-menu index="3">
                <template #title>
                  <el-icon>
                    <Tools />
                  </el-icon>
                  <span>系统信息管理</span>
                </template>
                <el-menu-item index="/dept">
                  <el-icon>
                    <HelpFilled />
                  </el-icon>
                  <span>部门管理</span>
                </el-menu-item>
                <el-menu-item index="/emp">
                  <el-icon>
                    <Avatar />
                  </el-icon>
                  <span>员工管理</span>
                </el-menu-item>
              </el-sub-menu>




              <!-- 数据统计管理 -->
              <el-sub-menu index="4">
                <template #title>
                  <el-icon>
                    <Histogram />
                  </el-icon>
                  <span>数据统计管理</span>
                </template>
                <el-menu-item index="/report/emp">
                  <el-icon>
                    <InfoFilled />
                  </el-icon>
                  <span>员工信息统计</span>
                </el-menu-item>
                <el-menu-item index="/report/stu">
                  <el-icon>
                    <Share />
                  </el-icon>
                  <span>学员信息统计</span>
                </el-menu-item>
                <el-menu-item index="/log">
                  <el-icon>
                    <Document />
                  </el-icon>
                  <span>日志信息统计</span>
                </el-menu-item>
              </el-sub-menu>




            </el-menu>
          </el-scrollbar>
        </el-aside>

        <!-- 核心展示区 -->
        <el-main>
          <router-view></router-view>
        </el-main>
      </el-container>

    </el-container>
  </div>
</template>

<style scoped>
.header {
  background-image: linear-gradient(to right, #00547d, #007fa4, #00aaa0, #00d072, #a8eb12);
}

.title {
  color: white;
  font-size: 40px;
  font-family: 楷体;
  line-height: 60px;
  font-weight: bolder;
}

.right_tool {
  float: right;
  line-height: 60px;
}

a {
  color: white;
  text-decoration: none;
}

.aside {
  width: 220px;
  border-right: 1px solid #ccc;
  height: 730px;
}
</style>

修改@/router/index.js文件,添加路由

import { createRouter, createWebHistory } from 'vue-router'

import ClazzView from '@/views/clazz/index.vue'
import DeptView from '@/views/dept/index.vue'
import EmpView from '@/views/emp/index.vue'
import IndexView from '@/views/index/index.vue'
import LayoutView from '@/views//layout/index.vue'
import LogView from '@/views//log/index.vue'
import LoginView from '@/views/login/index.vue'
import ReportEmpView from '@/views/report/emp/index.vue'
import ReportStuView from '@/views/report/stu/index.vue'
import StuView from '@/views/stu/index.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {path: '/index',name: 'index',component: IndexView},
    {path: '/clazz',name: 'clazz',component: ClazzView},
    {path: '/dept',name: 'dept',component: DeptView},
    {path: '/emp',name: 'emp',component: EmpView},,
    {path: '/log',name: 'log',component: LogView},
    {path: '/login',name: 'login',component: LoginView},
    {path: '/report/emp',name: 'reportEmp',component: ReportEmpView},
    {path: '/report/stu',name: 'reportStu',component: ReportStuView},
    {path: '/stu',name: 'stu',component: StuView},
  ]
})

export default router

注:上述会出现一个问题,访问/login登录页面会在核心加载区域显示

3.嵌套路由(解决login页面加载问题)

修改@/App.vue

<script setup>
// //引入views/layout/index.vue命名为Layout
// import Layout from "@/views/layout/index.vue";
</script>

<template>
  <!-- <Layout></Layout> -->
   <router-view></router-view>
</template>

<style scoped>

</style>

修改@/router/index.js

import { createRouter, createWebHistory } from 'vue-router'

import ClazzView from '@/views/clazz/index.vue'
import DeptView from '@/views/dept/index.vue'
import EmpView from '@/views/emp/index.vue'
import IndexView from '@/views/index/index.vue'
import LayoutView from '@/views//layout/index.vue'
import LogView from '@/views//log/index.vue'
import LoginView from '@/views/login/index.vue'
import ReportEmpView from '@/views/report/emp/index.vue'
import ReportStuView from '@/views/report/stu/index.vue'
import StuView from '@/views/stu/index.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'layout',
      component: LayoutView,
      // redirect 如果没有匹配到任意子组件,重定向到 /index 组件
      redirect: '/index',
      children: [
        {path: 'index',name: 'index',component: IndexView},
        {path: 'clazz',name: 'clazz',component: ClazzView},
        {path: 'dept',name: 'dept',component: DeptView},
        {path: 'emp',name: 'emp',component: EmpView},
        {path: 'log',name: 'log',component: LogView},
        {path: 'report/emp',name: 'reportEmp',component: ReportEmpView},
        {path: 'report/stu',name: 'reportStu',component: ReportStuView},
        {path: 'stu',name: 'stu',component: StuView},
      ]
    },
    {path: '/login',name: 'login',component: LoginView},

  ]
})

export default router

上述中,/下面中children中对应的目录会交给LayoutView组件渲染,/login单独渲染

4.部门管理列表

  1. 首先使用element-plus官网查找按钮和表格,完成页面的基本设计
  2. 实现搜索功能,页面加载使用钩子方法自动加载

@/views/dept/index.vue

<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'

// 钩子函数
onMounted(() => {
  search();
})

const search = async () => {
  const result = await axios.get('https://m1.apifoxmock.com/m1/6450352-6149047-default/depts');
  if (result.data.code == 1) {
    tableData.value = result.data.data;
  }
}

const tableData = ref([])
</script>

<template>
  <h1>部门管理</h1>
  <div class="container">
    <el-button type="primary">+ 新增部门</el-button>
  </div>

  <div class="container">
    <el-table :data="tableData" style="width: 100%" border>
      <!-- 1. 使用数据库返回的 id 作为索引列->
      <!-- <el-table-column prop="id" label="ID" width="180" align="center" /> -->
      <!-- 2. 使用el-table-column type="index" 索引列 -->
      <el-table-column type='index' label="部门名称" width="180" align="center" />
      <el-table-column prop="name" label="部门名称" width="180" align="center" />
      <el-table-column prop="updateTime" label="最后操作时间" align="center" />
      <el-table-column label="操作" align="center">
        <template #default="" scope>
          <div class="mb-4">
            <el-button type="warning" text>
              <el-icon>
                <Edit />
              </el-icon>编辑
            </el-button>
            <el-button type="danger" text>
              <el-icon>
                <Delete />
              </el-icon>删除
            </el-button>
          </div>
        </template>
      </el-table-column>
    </el-table>
  </div>

</template>

<style scoped>
.container {
  margin-top: 15px;
}
</style>

5.axios优化

上述部门管理查询时,在单独的vue组件中使用axios,如果说一个项目拥有多个接口的请求,需要在每个vue组件中都需要导入axios多次调用。

  • 为了解决上述问题,一般在项目开发的时候,通常会定义请求处理的工具类 - request.js
  • 与服务器进行异步交互的逻辑,通常会封装到一个单独的api中,例如: dept.js
  1. 首先在src目录创建utilsapi目录

  2. 接着在utils目录中创建request.js文件,内容如下

    import axios from 'axios'
    
    //创建axios实例对象
    const request = axios.create({
      baseURL: 'https://m1.apifoxmock.com/m1/6450352-6149047-default',
      timeout: 600000
    })
    
    //axios的响应 response 拦截器
    request.interceptors.response.use(
      (response) => { //成功回调
        return response.data
      },
      (error) => { //失败回调
        return Promise.reject(error)
      }
    )
    
    export default request
    
  3. 然后在api目录中创建dept.js,内容如下

    import request from '@/utils/request'
    
    // 查询全部部门数据
    // export const queryAllApi = () => {
    //     return request.get('/depts');
    // }
    
    // 可以省略return
    export const queryAllApi = () => request.get('/depts');
    

api目录主要负责前端与后端交互的部分

  1. 修改部门管理页面使用优化有的api方式

    @/views/dept/index.vue

    <script setup>
    import { ref, onMounted } from 'vue'
    // 导入api
    import { queryAllApi } from '@/api/dept'
    
    // 钩子函数
    onMounted(() => {
      search();
    })
    
    const search = async () => {
      // const result = await axios.get('https://m1.apifoxmock.com/m1/6450352-6149047-default/depts');
      // if (result.data.code == 1) {
      //   tableData.value = result.data.data;
      // }
    
      // 调用api
      const result = await queryAllApi();
      if (result.code == 1) {
        tableData.value = result.data;
      }
    }
    
    const tableData = ref([])
    </script>
    
    <template>
      <h1>部门管理</h1>
      <div class="container">
        <el-button type="primary">+ 新增部门</el-button>
      </div>
    
      <div class="container">
        <el-table :data="tableData" style="width: 100%" border>
          <!-- 1. 使用数据库返回的 id 作为索引列->
          <!-- <el-table-column prop="id" label="ID" width="180" align="center" /> -->
          <!-- 2. 使用el-table-column type="index" 索引列 -->
          <el-table-column type='index' label="部门名称" width="180" align="center" />
          <el-table-column prop="name" label="部门名称" width="180" align="center" />
          <el-table-column prop="updateTime" label="最后操作时间" align="center" />
          <el-table-column label="操作" align="center">
            <template #default="" scope>
              <div class="mb-4">
                <el-button type="warning" text>
                  <el-icon>
                    <Edit />
                  </el-icon>编辑
                </el-button>
                <el-button type="danger" text>
                  <el-icon>
                    <Delete />
                  </el-icon>删除
                </el-button>
              </div>
            </template>
          </el-table-column>
        </el-table>
      </div>
    
    </template>
    
    <style scoped>
    .container {
      margin-top: 15px;
    }
    </style>
    

6.前后端优化

上述中request.js文件默认的baseURL是我们指定的一个路径

const request = axios.create({
  baseURL: '/api',
  timeout: 600000
})

在真实的项目开发中,一般请求的是本机的/api目录,使用代理的方式将/api目录转发到后端tomcat服务器的端口,使用vue项目的vite.config.js实现

配置如下:

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
 plugins: [vue()],
 resolve: {
   alias: {
     '@': fileURLToPath(new URL('./src', import.meta.url))
   }
 },
    
    
 // 修改server配置,访问/api目录代理,转发到tomcat请求数据
 // 转发前 http://localhost:5173/api/depts
 // 转发后 http://localhost:8080/depts
 server: {
   proxy: {
     '/api': {
       target: 'http://localhost:8080',
       secure: false,
       changeOrigin: true,
       rewrite: (path) => path.replace(/^\/api/, ''),
     }
   }
 }
})

7.新增部门

  1. 点击新增部门打开form表单
  2. 输入新增部门信息,点击保存按钮调用新增部门接口

@/api/dept.js

// 新增部门
export const addApi = (dept) => request.post('/depts', dept);

导入api

// 导入api
import { queryAllApi, addApi, queryByIdApi } from '@/api/dept'

新增员工Dialog对话框

  1. 在新增部门按钮添加@click="addDept"属性

  2. 添加Dialog对话框

    <script setup>
     之前的代码
    
    // Dialog对话框
    const dialogFormVisible = ref(false);  // 控制对话框状态
    const formTitle = ref();               // 控制对话框标题
    const dept = ref({ name: '' });        // 控制对话框默认表单数据
    
    // 新增部门
    const addDept = () => {
      dialogFormVisible.value = true;
      formTitle.value = '新增部门';
      dept.value = { name: '' }; // 每次打开新增窗口,清空表单数据
    
    
      // 重置表单校验规则--提示信息
      if(deptFormRef.value){
        deptFormRef.value.resetFields();
      }
    }
    
    
    
    const save = async () => {
      // 表单校验
      if (!deptFormRef.value) return;
      deptFormRef.value.validate(async (valid) => { // valid 表示是否校验通过
    
        if (valid) {  // 表单校验通过
          const result = await addApi(dept.value);
          if (result.code == 1) { // 成功
            // 提示信息
            ElMessage.success('操作成功');
            // 关闭对话框
            dialogFormVisible.value = false;
            // 查询
            search();
          } else {  // 失败
            ElMessage.error(result.msg);
          }
        }else{ // 表单校验不通过
            ElMessage.error('表单校验不通过');
    
        }
      })
    
        
    // 表单验证规则
    const rules = ref({
      name: [
        { required: true, message: '部门名称为必填项', trigger: 'blur' },
        { min: 2, max: 10, message: '部门名称长度必须在2-10之间', trigger: 'blur' },
      ]
    })
    
    const deptFormRef = ref();
    
    </script>
    
    
    <template>
        之前的代码
        
        
    	<!-- 新增员工对话框 Dialog对话框 -->
      <el-dialog v-model="dialogFormVisible" :title="formTitle" width="500">
        <!-- 使用rules属性,实现表单校验,ref属性实现校验逻辑 -->
        <el-form :model="dept" :rules="rules" ref="deptFormRef">
          <el-form-item label="部门名称" label-width="80px" prop="name">
            <el-input v-model="dept.name" autocomplete="off" />
          </el-form-item>
        </el-form>
        <template #footer>
          <div class="dialog-footer">
            <el-button @click="dialogFormVisible = false">取消</el-button>
            <el-button type="primary" @click="save">保存</el-button>
          </div>
        </template>
      </el-dialog>
    </template>
    

    上述中,

    • 首先完成添加Dialog对话框标签和基本开关、标题动态数据

    • 接着在form表单添加:rules="rules"属性动态绑定rules表单校验数组

    • ref="deptFormRef"属性绑定表单的逻辑校验

    • 最后在保存是调用save方法,首先校验表单的数据是否合法,接着进行用户的新增操作

      • 操作成功:成功操作提示,关闭Dialog对话框,页面刷新
      • 操作失败:失败操作提示

8.修改部门

  1. 点击编辑打开编辑部门form表单(含有当前部门信息)

    逻辑实现: 调用根据id查询部门信息接口

  2. 点击保存修改部门信息

    逻辑实现:调用修改部门信息接口

*@/api/dept.js,

// 根据id查询部门
export const queryByIdApi = (id) => request.get(`/depts/${id}`);


// 修改部门
export const updateApi = (dept) => request.put('/depts', dept);

在头部导入import { queryAllApi, addApi, queryByIdApi, updateApi } from '@/api/dept'

在编辑按钮加入@click="editDept(scope.row.id)"属性,scope.row.id表示获取当前行的索引

逻辑分析:

  1. 用户点击编辑按钮后,会根据当前id获取部门信息并返回
  2. 用户修改完数据,点击保存更改用户信息

注: 当前修改部门和新增部门使用的是同一个Dialog对话框,如何判断进行的操作是新增部门还是修改部门?

修改部门需要部门的id值,新增员工不需要,通过判断该值,可以判断是进行新增还是修改

@/views/dept/index.vue

<script setup>
之前的代码

const save = async () => {
  // 表单校验
  if (!deptFormRef.value) return;
  deptFormRef.value.validate(async (valid) => { // valid 表示是否校验通过

    if (valid) {  // 表单校验通过


      let result;   // const是常量不能重新复制,使用let
      // 根据id判断是修改还是新增
      if (dept.value.id) { // 修改
        result = await updateApi(dept.value);
      } else { // 新增
        result = await addApi(dept.value);
      }


      if (result.code == 1) { // 成功
        // 提示信息
        ElMessage.success('操作成功');
        // 关闭对话框
        dialogFormVisible.value = false;
        // 查询
        search();
      } else {  // 失败
        ElMessage.error(result.msg);
      }

    } else { // 表单校验不通过
      ElMessage.error('表单校验不通过');

    }
  })

}


// 修改部门按钮
const editDept = async (id) => {
  formTitle.value = '编辑部门';
  // 重置表单校验规则--提示信息
  if (deptFormRef.value) {
    deptFormRef.value.resetFields();
  }
  const result = await queryByIdApi(id);
  if (result.code == 1) { // 成功
    dialogFormVisible.value = true;
    dept.value = result.data;
  }
}

</script>

9.删除部门(页面布局)

新增删除接口

// 删除部门
export const deleteByIdApi = (id) => request.delete(`/depts?id=${id}`);

在删除按钮增加属性@click="deleteByDept(scope.row.id)"

element-plus找到新的Dialog对话框,因为删除操作比较危险,所以在点击删除时,需要弹出是否确定删除的按钮

@/views/dept/index.vue

<script setup>
import { ref, onMounted } from 'vue'
// 导入api
import { queryAllApi, addApi, queryByIdApi, updateApi, deleteByIdApi } from '@/api/dept'
// 导入ElMessage
import { ElMessage, ElMessageBox } from 'element-plus'

之前的代码

// 删除部门
const deleteByDept = async (id) => {
  // 弹出确认框
  ElMessageBox.confirm(
    '你确定要删除该部门吗?',
    '提示',
    {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning',
    }
  ).then(async () => { // 确认
    const result = await deleteByIdApi(id)
    if (result.code) { // 查询成功
      ElMessage.success('删除成功');
      search();
    } else { // 查询失败
      ElMessage.error(result.msg);
    }
  }).catch(() => { // 取消
    // ElMessage({type: 'info',message: '删除取消',})
    ElMessage.info('删除取消');
  })
}

</script>

10.员工页面布局

搜索表单

  1. 首先在element-plus官网找到表单组件,并设计搜索表单
  2. 根据搜索表单设计双向数据绑定searchForm
  3. 写好searchclear按钮的基础功能

@/views/emp/index.vue

<script setup>
import { ref } from 'vue';

const searchForm = ref({ name: '', gender: '', createTime: [] });

// 查询
const search = () => {
  console.log(searchForm.value);
}


// 清空
const clear = () => {
  searchForm.value = { name: '', gender: '', createTime: [] };
}

</script>

<template>
  <h1>员工管理</h1>

  <!-- 表单 -->
  <div class="container">
    <el-form :inline="true" :model="formInline" class="demo-form-inline">
      <el-form-item label="姓名">
        <el-input v-model="searchForm.name" placeholder="请输入员工姓名" clearable />
      </el-form-item>

      <el-form-item label="性别">
        <el-select v-model="searchForm.gender" placeholder="请选择" clearable>
          <el-option label="男" value="1" />
          <el-option label="女" value="2" />
        </el-select>
      </el-form-item>

      <el-form-item label="入职日期">
        <el-col>
          <el-form-item prop="date1">
            <el-date-picker v-model="searchForm.createTime"
            type="daterange" 
            range-separator="To" 
            start-placeholder="开始日期"
            end-placeholder="结束日期" 
            value-format="YYYY-MM-DD"/>
          </el-form-item>
        </el-col>
      </el-form-item>

      <el-form-item>
        <el-button type="primary" @click="search">查询</el-button>
        <el-button type="info" @click="clear">清空</el-button>
      </el-form-item>

    </el-form>
  </div>
</template>

<style scoped>
.container {
  margin: 15px 0px;
}
</style>

watch侦听

作用: 侦听一个或多个响应式数据源,并在数据源发生变化时调用传入的回调函数

用法:

  1. 导入watch函数
  2. 执行watch函数,传入要侦听的响应式数据源(ref对象)和回调函数。

侦听示例:

const a = ref(1);
watch(a,(newVal,oldVal) => {
  console.log(`更改前的值: ${oldVal},更改后的值: ${newVal}`)
})

上述中,只需要更改变量a的值,就会出发回调函数

侦听对象全部属性

// 深度侦听 设置 deep: true 
// 侦听对象的全部属性
const user = ref({ name: '', age: 1 });
watch(user,(newVal,oldVale) => {  // 侦听user对象中的全部属性的变化
  console.log(newVal);
}, {deep: true})

侦听对象的单个属性

// 侦听对象的单个属性
const user = ref({ name: '', age: 1 });
watch(() => user.value.age,(newVal,oldVale) => {  // 侦听user对象中age属性的变化
  console.log(`变化前的值: ${oldVale},变化后的值: ${newVal}`);
})

实现入职时间变化时,将开始日期赋值给begin、结束日期赋值给end

<script setup>
import { ref, watch } from 'vue';

const searchForm = ref({ name: '', gender: '', createTime: [], begin: '', end: '' });

// 查询
const search = () => {
  // console.log(searchForm.value);
}


// 清空
const clear = () => {
  searchForm.value = { name: '', gender: '', createTime: [], begin: '', end: '' };
}


// watch侦听--------------------演示------------------
// const a = ref(1);
// watch(a,(newVal,oldVal) => {
//   console.log(`更改前的值: ${oldVal},更改后的值: ${newVal}`)
// })


// // 深度侦听 设置 deep: true 
// // 侦听对象的全部属性
// const user = ref({ name: '', age: 1 });
// watch(user,(newVal,oldVale) => {  // 侦听user对象中的全部属性的变化
//   console.log(newVal);
// }, {deep: true})


// // 侦听对象的单个属性
// const user = ref({ name: '', age: 1 });
// watch(() => user.value.age,(newVal,oldVale) => {  // 侦听user对象中age属性的变化
//   console.log(`变化前的值: ${oldVale},变化后的值: ${newVal}`);
// })



// 使用侦听searchForm.createTime变化时,将第一个值赋值给begin,第二个值赋值给end
watch(() => searchForm.value.createTime, (newVal, oldVal) => {
  if (newVal.length == 2) {
    searchForm.value.begin = newVal[0];
    searchForm.value.end = newVal[1];
  } else {
    searchForm.value.begin = '';
    searchForm.value.end = '';
  }

})

</script>

按钮、表格、分页条

剩余功能按钮,表格,以及分页条

<script setup>
之前的代码


const empList = ref([
  {
    "id": 1,
    "username": "jinyong",
    "password": "123456",
    "name": "金庸",
    "gender": 1,
    "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-02-00-27-53B.jpg",
    "job": 2,
    "salary": 8000,
    "entryDate": "2015-01-01",
    "deptId": 2,
    "deptName": "教研部",
    "createTime": "2022-09-01T23:06:30",
    "updateTime": "2022-09-02T00:29:04"
  }
])




// 分页条
const currentPage = ref(1)  // 页码
const pageSize = ref(10)    // 每页展示记录数
const background = ref(true)  //是否展示背景色
const total = ref(400)


// 每页展示记录数变化时触发
const handleSizeChange = (size) => {
  console.log(`每页展示记录数: ${size}`)
}

// 页码变化时触发
const handleCurrentChange = (val) => {
  console.log(`当前页: ${val}`)
}




</script>

<template>
  之前的代码

  <!-- 功能按钮 -->
  <div class="container">
    <el-button type="primary">+ 新增员工</el-button>
    <el-button type="danger">- 批量删除</el-button>
  </div>

  <!-- 表格 -->
  <div class="container">
    <el-table :data="empList" border style="width: 100%">
      <el-table-column prop="name" label="姓名" width="120" align="center" />
      <el-table-column label="性别" width="120" align="center">
        <template #default="scope">
          <span v-if="scope.row.gender == 1">男</span>
          <span v-else-if="scope.row.gender == 2">女</span>
          <span v-else>其他</span>
        </template>
      </el-table-column>
      <el-table-column label="头像" width="120" align="center">
        <template #default="scope">
          <!-- <img :src="scope.row.image" style="width: 50px; height: 50;" /> -->
          <el-avatar :src="scope.row.image" />
        </template>
      </el-table-column>
      <el-table-column prop="deptName" label="所属部门" width="120" align="center" />
      <el-table-column prop="job" label="职位" width="120" align="center">
        <template #default="scope">
          <span v-if="scope.row.job == 1">班主任</span>
          <span v-else-if="scope.row.job == 2">讲师</span>
          <span v-else-if="scope.row.job == 3">学工主管</span>
          <span v-else-if="scope.row.job == 4">教研主管</span>
          <span v-else-if="scope.row.job == 5">咨询师</span>
          <span v-else>其他</span>
        </template>
      </el-table-column>
      <el-table-column prop="entryDate" label="入职日期" width="180" align="center" />
      <el-table-column prop="updateTime" label="最后操作时间" width="200" align="center" />
      <el-table-column label="操作" align="center">
        <template #default="scope">
          <div class="mb-4">
            <!-- 点击编辑按钮,获取当前部门id参数给editDept方法 -->
            <el-button type="warning" text>
              <el-icon>
                <Edit />
              </el-icon>编辑
            </el-button>
            <el-button type="danger" text>
              <el-icon>
                <Delete />
              </el-icon>删除
            </el-button>
          </div>
        </template>
      </el-table-column>
    </el-table>
  </div>

  <!-- 分页条 -->
  <div class="demo-pagination-block" width="100%">
    <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[10, 20, 50, 100]"
      :background=background layout="sizes, total, prev, pager, next, jumper" :total=total
      @size-change="handleSizeChange" @current-change="handleCurrentChange" />
  </div>


</template>

11.员工页面基本功能实现

主要完成员工管理页面的数据查询功能

@/api/emp.js

import request from '@/utils/request'


// 查询所有员工
export const queryAllApi = (name,gender,begin,end,page,pageSize) => 
    request.get(`/emps?name=${name}&gender=${gender}&begin=${begin}&end=${end}&page=${page}&pageSize=${pageSize}`)

@/views/emp/index.vue

<script setup>
import { ref, watch,onMounted } from 'vue';
import { queryAllApi } from '../../api/emp';

const searchForm = ref({ name: '', gender: '', createTime: [], begin: '', end: '' });
const empList = ref([]);

// 查询
const search = async () => {
  const result = await queryAllApi(searchForm.value.name,searchForm.value.gender,searchForm.value.begin,searchForm.value.end,currentPage.value,pageSize.value);
  if(result.code){
    empList.value = result.data.rows;
    total.value = result.data.total;
  }

}

// 清空
const clear = () => {
  searchForm.value = { name: '', gender: '', createTime: [], begin: '', end: '' };
  search();
}

// 钩子函数
onMounted(() => {
  search();
})



// 使用侦听searchForm.createTime变化时,将第一个值赋值给begin,第二个值赋值给end
watch(() => searchForm.value.createTime, (newVal, oldVal) => {
  if (newVal.length == 2) {
    searchForm.value.begin = newVal[0];
    searchForm.value.end = newVal[1];
  } else {
    searchForm.value.begin = '';
    searchForm.value.end = '';
  }
})



// 分页条
const currentPage = ref(1)  // 页码
const pageSize = ref(10)    // 每页展示记录数
const background = ref(true)  //是否展示背景色
const total = ref(400)


// 每页展示记录数变化时触发
const handleSizeChange = (size) => {
  search();
}

// 页码变化时触发
const handleCurrentChange = (val) => {
  search();
}

</script>

12.新增员工

首先完成新增员工的页面布局,然后完成新增员工的逻辑实现

页面布局

完成点击新增员工按钮,打开Dialog对话框,显示新增员工的基本模型

<script setup>
import { ref, watch, onMounted } from 'vue';
import { queryAllApi } from '../../api/emp';

之前的代码


//新增/修改表单
const employee = ref({
  username: '',
  name: '',
  gender: '',
  phone: '',
  job: '',
  salary: '',
  deptId: '',
  entryDate: '',
  image: '',
  exprList: []
})

// 控制弹窗
const dialogVisible = ref(false)
const dialogTitle = ref('新增员工')

// 新增员工
const addEmp = () => {
  dialogVisible.value = true;
  dialogTitle.value = '新增员工';
}


</script>

<template>
	之前的代码  

  <!-- 新增员工Dialog -->
    <el-dialog v-model="dialogVisible" :title="dialogTitle">
      <el-form :model="employee" label-width="80px">
        <!-- 基本信息 -->
        <!-- 第一行 -->
         <!-- layout布局中 在行属性中配置gutter控制每列之间的距离,若不配置,默认值为0f -->
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="用户名">
              <el-input v-model="employee.username" placeholder="请输入员工用户名,2-20个字"></el-input>
            </el-form-item>
          </el-col>
          
          <el-col :span="12">
            <el-form-item label="姓名">
              <el-input v-model="employee.name" placeholder="请输入员工姓名,2-10个字"></el-input>
            </el-form-item>
          </el-col>
        </el-row>

        <!-- 第二行 -->
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="性别">
              <el-select v-model="employee.gender" placeholder="请选择性别" style="width: 100%;">
                <el-option label="男" value="1"></el-option>
                <el-option label="女" value="2"></el-option>
              </el-select>
            </el-form-item>
          </el-col>

          <el-col :span="12">
            <el-form-item label="手机号">
              <el-input v-model="employee.phone" placeholder="请输入员工手机号"></el-input>
            </el-form-item>
          </el-col>
        </el-row>

        <!-- 第三行 -->
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="职位">
              <el-select v-model="employee.job" placeholder="请选择职位" style="width: 100%;">
                <el-option label="班主任" value="1"></el-option>
                <el-option label="讲师" value="2"></el-option>
                <el-option label="学工主管" value="3"></el-option>
                <el-option label="教研主管" value="4"></el-option>
                <el-option label="咨询师" value="5"></el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="薪资">
              <el-input v-model="employee.salary" placeholder="请输入员工薪资"></el-input>
            </el-form-item>
          </el-col>
        </el-row>

        <!-- 第四行 -->
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="所属部门">
              <el-select v-model="employee.deptId" placeholder="请选择部门" style="width: 100%;">
                <el-option label="研发部" value="1"></el-option>
                <el-option label="市场部" value="2"></el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="入职日期">
              <el-date-picker v-model="employee.entryDate" type="date" style="width: 100%;" placeholder="选择日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD"></el-date-picker>
            </el-form-item>
          </el-col>
        </el-row>

        <!-- 第五行 -->
        <el-row :gutter="20">
          <el-col :span="24">
            <el-form-item label="头像">
              <el-upload
                class="avatar-uploader"
                action="/api/upload"
                :show-file-list="false"
                :on-success="handleAvatarSuccess"
                :before-upload="beforeAvatarUpload"
                >
                <img v-if="employee.image" :src="employee.image" class="avatar" />
                <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
              </el-upload>
            </el-form-item>
          </el-col>
        </el-row>
        

        <!-- 工作经历 -->
        <!-- 第六行 -->
        <el-row :gutter="10">
          <el-col :span="24">
            <el-form-item label="工作经历">
              <el-button type="success" size="small" @click="">+ 添加工作经历</el-button>
            </el-form-item>
          </el-col>
        </el-row>
        
        <!-- 第七行 ...  工作经历 -->
        <el-row :gutter="3">
          <el-col :span="10">
            <el-form-item size="small" label="时间" label-width="80px">
              <el-date-picker type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" ></el-date-picker>
            </el-form-item>
          </el-col>

          <el-col :span="6">
            <el-form-item size="small" label="公司" label-width="60px">
              <el-input placeholder="请输入公司名称"></el-input>
            </el-form-item>
          </el-col>

          <el-col :span="6">
            <el-form-item size="small" label="职位" label-width="60px">
              <el-input placeholder="请输入职位"></el-input>
            </el-form-item>
          </el-col>

          <el-col :span="2">
            <el-form-item size="small" label-width="0px">
              <el-button type="danger" >- 删除</el-button>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      
      <!-- 底部按钮 -->
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="">保存</el-button>
        </span>
      </template>
  </el-dialog>

</template>

<style scoped>
之前的代码

.avatar {
  height: 40px;
}
.avatar-uploader .avatar {
  width: 78px;
  height: 78px;
  display: block;
}
.avatar-uploader .el-upload {
  border: 1px dashed var(--el-border-color);
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: var(--el-transition-duration-fast);
}

.avatar-uploader .el-upload:hover {
  border-color: var(--el-color-primary);
}

.el-icon.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 78px;
  height: 78px;
  text-align: center;
  border-radius: 10px;
  /* 添加灰色的虚线边框 */
  border: 1px dashed var(--el-border-color);
}
</style>

性别、职位代码优化

上述中,在写配置性别、职位、所属部门代码的时候,直接在代码中写死了,在后期开发中,不方便维护,如下:

<el-col :span="12">
    <el-form-item label="职位">
      <el-select v-model="employee.job" placeholder="请选择职位" style="width: 100%;">
        <el-option label="班主任" value="1"></el-option>
        <el-option label="讲师" value="2"></el-option>
        <el-option label="学工主管" value="3"></el-option>
        <el-option label="教研主管" value="4"></el-option>
        <el-option label="咨询师" value="5"></el-option>
      </el-select>
    </el-form-item>
</el-col>

例如上述使用v-for优化后

<script setup>
//职位列表数据
const jobs = ref([{ name: '班主任', value: 1 },{ name: '讲师', value: 2 },{ name: '学工主管', value: 3 },{ name: '教研主管', value: 4 },{ name: '咨询师', value: 5 },{ name: '其他', value: 6 }])
</script>

<template>
<!-- 第三行 -->
<el-row :gutter="20">
    <el-col :span="12">
      <el-form-item label="职位">
        <el-select v-model="employee.job" placeholder="请选择职位" style="width: 100%;">
          <el-option v-for="j in jobs" :key="j.value" :label="j.name" :value="j.value"></el-option>
        </el-select>
      </el-form-item>
    </el-col>
</el-row>
</template>

新增员工需要选择所属部门,上述中已经开发了部门接口,在选择部门的时候,可以直接请求部门接口获取全部部门

优化后代码如下

<script setup>
import { queryAllApi as queryAllDeptApi } from "@/api/dept";

//职位列表数据
const jobs = ref([{ name: '班主任', value: 1 },{ name: '讲师', value: 2 },{ name: '学工主管', value: 3 },{ name: '教研主管', value: 4 },{ name: '咨询师', value: 5 },{ name: '其他', value: 6 }])
//性别列表数据
const genders = ref([{ name: '男', value: 1 }, { name: '女', value: 2 }])
//部门列表数据
const depts = ref([]);


// 钩子函数
onMounted(() => {
  search();
  searchDept();
})
    
    
    
// 查询部门信息
const searchDept = async () => {
  const result = await queryAllDeptApi();
  if (result.code) {
    depts.value = result.data;
  }
}
</script>


<template>
        <!-- 第二行 -->
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="性别">
              <el-select v-model="employee.gender" placeholder="请选择性别" style="width: 100%;">
                <el-option v-for="g in genders" :key="g.value" :label="g.name" :value="g.value"></el-option>
              </el-select>
            </el-form-item>
          </el-col>

          <el-col :span="12">
            <el-form-item label="手机号">
              <el-input v-model="employee.phone" placeholder="请输入员工手机号"></el-input>
            </el-form-item>
          </el-col>
        </el-row>

        <!-- 第三行 -->
        <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="职位">
                <el-select v-model="employee.job" placeholder="请选择职位" style="width: 100%;">
                  <el-option v-for="j in jobs" :key="j.value" :label="j.name" :value="j.value"></el-option>
                </el-select>
              </el-form-item>
            </el-col>
          <el-col :span="12">
            <el-form-item label="薪资">
              <el-input v-model="employee.salary" placeholder="请输入员工薪资"></el-input>
            </el-form-item>
          </el-col>
        </el-row>

        <!-- 第四行 -->
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="所属部门">
              <el-select v-model="employee.deptId" placeholder="请选择部门" style="width: 100%;">
                <el-option v-for="d in depts" :key="d.id" :label="d.name" :value="d.id"></el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="入职日期">
              <el-date-picker v-model="employee.entryDate" type="date" style="width: 100%;" placeholder="选择日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD"></el-date-picker>
            </el-form-item>
          </el-col>
        </el-row>
</template>

文件上传功能

使用element-plus提供的文件上传组件,部分代码如下

<script setup>
之前的代码


//文件上传
// 图片上传成功后触发
const handleAvatarSuccess = (response) => {
  const result = response;
  if(result.code){
    employee.value.image = result.data;
  }

}
// 文件上传之前触发
const beforeAvatarUpload = (rawFile) => {
  if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') {
    ElMessage.error('只支持上传图片')
    return false
  } else if (rawFile.size / 1024 / 1024 > 10) {
    ElMessage.error('只能上传10M以内图片')
    return false
  }
  return true
}


</script>

添加、删除工作经历

功能实现:

  • 每次点击添加工作经历,增加一个工作经历信息
  • 点击删除,删除指定的工作经历信息

代码流程如下:

  1. 在添加员工工作经历按钮标签增加属性@click="addEmpExpr"
  2. 编写addEmpExpr方法代码
  3. 使用v-for配合v-model优化员工多条工作经历显示
  4. 在删除员工工作经历按钮标签增加属性@click="deleteEmpExpr(index)",获取循环次数的index参数,方便删除
  5. 使用watch将获取的起止时间dataTime赋值给begin和end
<script setup>
之前的代码

// 新增员工工作经历
const addEmpExpr = () => {
  employee.value.exprList.push({ "company": "", "job": "", "begin": "", "end": "", dataTime: [] })
}

// 删除员工工作经历
const deleteEmpExpr = (index) => {
  // splice 从第几个开始删除,删除几个
  employee.value.exprList.splice(index,1);
}


// 侦听employee.exprList的变化,将exprList中的dataTime的值赋值给begin和end
watch(() => employee.value.exprList, (newVal, oldVal) => {
  if (employee.value.exprList && employee.value.exprList.length > 0) {
    employee.value.exprList.forEach((expr) => {
      expr.begin = expr.dataTime[0];
      expr.end = expr.dataTime[1];
    })
  }
}, { deep: true });


</script>

<template>
      <!-- 第七行 ...  工作经历 -->
      <el-row :gutter="3" v-for="(expr, index) in employee.exprList">
        <el-col :span="10">
          <el-form-item size="small" label="时间" label-width="80px">
            <el-date-picker type="daterange" v-model="expr.dataTime" range-separator="至" start-placeholder="开始日期"
              end-placeholder="结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD"></el-date-picker>
          </el-form-item>
        </el-col>

        <el-col :span="6">
          <el-form-item size="small" label="公司" label-width="60px">
            <el-input placeholder="请输入公司名称" v-model="expr.company"></el-input>
          </el-form-item>
        </el-col>

        <el-col :span="6">
          <el-form-item size="small" label="职位" label-width="60px">
            <el-input placeholder="请输入职位" v-model="expr.job"></el-input>
          </el-form-item>
        </el-col>

        <el-col :span="2">
          <el-form-item size="small" label-width="0px">
            <el-button type="danger" @click="deleteEmpExpr(index)">- 删除</el-button>
          </el-form-item>
        </el-col>
      </el-row>
</template>

新增员工功能实现

  1. 点击保存,发送异步请求到服务器,提交数据。
  2. 保存完毕后,如果成功,关闭对话框,重新加载列表数据。
  3. 保存完毕后,如果失败,提示错误信息。

@/api/emp.js

// 新增员工
export const addApi = (emp) => request.post("/emps",emp);

@/views/emp/index.vue

<script setup>
// 新增员工
const addEmp = () => {
  dialogVisible.value = true;
  dialogTitle.value = '新增员工';
  employee.value = {username: '', name: '', gender: '', phone: '', job: '', salary: '', deptId: '', entryDate: '', image: '', exprList: []}
}


// 保存员工信息
const save = async () => {
  const result = await addApi(employee.value);
  if(result.code){  // 成功
    ElMessage.success("保存成功")
    dialogVisible.value = false;
    search();
  }else{  // 失败
    ElMessage.error(result.msg);
  }
}


</script>

新增员工表单校验

表单校验流程:

  1. 首先定义校验规则
  2. form表单引用规则
    • 表单rules属性,input等标签使用prop选择校验规则
  3. 在表单提交时进行表单校验
    • 表单ref属性,通过判断ref属性的动态数据进行校验
  4. 重置表单校验规则

具体流程如下:

首先定义校验规则和表单校验动态数据

//表单校验规则
const rules = ref({
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 2, max: 20, message: '用户名长度应在2到20个字符之间', trigger: 'blur' }
  ],
  name: [
    { required: true, message: '请输入姓名', trigger: 'blur' },
    { min: 2, max: 10, message: '姓名长度应在2到10个字符之间', trigger: 'blur' }
  ],
  gender: [
    { required: true, message: '请选择性别', trigger: 'change' }
  ],
  phone: [
    { required: true, message: '请输入手机号', trigger: 'blur' },
    { pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号', trigger: 'blur' }
  ]
});

// 表单校验动态数据
const empFormRef = ref();

接着在表单使用改规则,注意表单的rulesref属性和内部文本框、复选框内的prop属性

  <!-- 新增员工Dialog -->
  <el-dialog v-model="dialogVisible" :title="dialogTitle">
    <el-form :model="employee" label-width="80px" :rules="rules" ref="empFormRef">
        
        
        
        
        
        
        
      <!-- {{ employee }} -->
      <!-- 基本信息 -->
      <!-- 第一行 -->
      <!-- layout布局中 在行属性中配置gutter控制每列之间的距离,若不配置,默认值为0f -->
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="用户名" prop="username">
            <el-input v-model="employee.username" placeholder="请输入员工用户名,2-20个字"></el-input>
          </el-form-item>
        </el-col>

        <el-col :span="12">
          <el-form-item label="姓名" prop="name">
            <el-input v-model="employee.name" placeholder="请输入员工姓名,2-10个字"></el-input>
          </el-form-item>
        </el-col>
      </el-row>

      <!-- 第二行 -->
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="性别" prop="gender">
            <el-select v-model="employee.gender" placeholder="请选择性别" style="width: 100%;">
              <el-option v-for="g in genders" :key="g.value" :label="g.name" :value="g.value"></el-option>
            </el-select>
          </el-form-item>
        </el-col>

        <el-col :span="12">
          <el-form-item label="手机号" prop="phone">
            <el-input v-model="employee.phone" placeholder="请输入员工手机号"></el-input>
          </el-form-item>
        </el-col>
      </el-row>

      <!-- 第三行 -->
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="职位">
            <el-select v-model="employee.job" placeholder="请选择职位" style="width: 100%;">
              <el-option v-for="j in jobs" :key="j.value" :label="j.name" :value="j.value"></el-option>
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="薪资">
            <el-input v-model="employee.salary" placeholder="请输入员工薪资"></el-input>
          </el-form-item>
        </el-col>
      </el-row>

      <!-- 第四行 -->
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="所属部门">
            <el-select v-model="employee.deptId" placeholder="请选择部门" style="width: 100%;">
              <el-option v-for="d in depts" :key="d.id" :label="d.name" :value="d.id"></el-option>
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="入职日期">
            <el-date-picker v-model="employee.entryDate" type="date" style="width: 100%;" placeholder="选择日期"
              format="YYYY-MM-DD" value-format="YYYY-MM-DD"></el-date-picker>
          </el-form-item>
        </el-col>
      </el-row>

      <!-- 第五行 -->
      <el-row :gutter="20">
        <el-col :span="24">
          <el-form-item label="头像">
            <el-upload class="avatar-uploader" action="/api/upload" :show-file-list="false"
              :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
              <img v-if="employee.image" :src="employee.image" class="avatar" />
              <el-icon v-else class="avatar-uploader-icon">
                <Plus />
              </el-icon>
            </el-upload>
          </el-form-item>
        </el-col>
      </el-row>


      <!-- 工作经历 -->
      <!-- 第六行 -->
      <el-row :gutter="10">
        <el-col :span="24">
          <el-form-item label="工作经历">
            <el-button type="success" size="small" @click="addEmpExpr">+ 添加工作经历</el-button>
          </el-form-item>
        </el-col>
      </el-row>

      <!-- 第七行 ...  工作经历 -->
      <el-row :gutter="3" v-for="(expr, index) in employee.exprList">
        <el-col :span="10">
          <el-form-item size="small" label="时间" label-width="80px">
            <el-date-picker type="daterange" v-model="expr.dataTime" range-separator="至" start-placeholder="开始日期"
              end-placeholder="结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD"></el-date-picker>
          </el-form-item>
        </el-col>

        <el-col :span="6">
          <el-form-item size="small" label="公司" label-width="60px">
            <el-input placeholder="请输入公司名称" v-model="expr.company"></el-input>
          </el-form-item>
        </el-col>

        <el-col :span="6">
          <el-form-item size="small" label="职位" label-width="60px">
            <el-input placeholder="请输入职位" v-model="expr.job"></el-input>
          </el-form-item>
        </el-col>

        <el-col :span="2">
          <el-form-item size="small" label-width="0px">
            <el-button type="danger" @click="deleteEmpExpr(index)">- 删除</el-button>
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>

    <!-- 底部按钮 -->
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="save">保存</el-button>
      </span>
    </template>
  </el-dialog>

在保存方法校验表单数据是否合法

// 保存员工信息
const save = async () => {
  // 表单校验
  if (!empFormRef.value) return;
  empFormRef.value.validate(async (valid) => { // valid 表示是否校验通过
    if (valid) {  // 表单校验通过

      const result = await addApi(employee.value);
      if (result.code) {  // 成功
        ElMessage.success("保存成功")
        dialogVisible.value = false;
        search();
      } else {  // 失败
        ElMessage.error(result.msg);
      }

    } else { // 表单校验不通过
      ElMessage.error('表单校验不通过');

    }
  })
}

每次打开重置表单校验

// 新增员工
const addEmp = () => {
  dialogVisible.value = true;
  dialogTitle.value = '新增员工';
  employee.value = { username: '', name: '', gender: '', phone: '', job: '', salary: '', deptId: '', entryDate: '', image: '', exprList: [] }

  // 重置表单校验规则--提示信息
  if (empFormRef.value) {
    empFormRef.value.resetFields();
  }
}

完整代码如下

<script setup>
import { ref, watch, onMounted } from 'vue';
import { queryAllApi, addApi } from '../../api/emp';
import { queryAllApi as queryAllDeptApi } from "@/api/dept";
import { ElMessage } from 'element-plus';

// 搜索表单信息
const searchForm = ref({ name: '', gender: '', createTime: [], begin: '', end: '' });

// 员工信息
const empList = ref([])


// 分页条
const currentPage = ref(1)  // 页码
const pageSize = ref(10)    // 每页展示记录数
const background = ref(true)  //是否展示背景色
const total = ref(400)



//新增/修改表单
const employee = ref({
  username: '',
  name: '',
  gender: '',
  phone: '',
  job: '',
  salary: '',
  deptId: '',
  entryDate: '',
  image: '',
  exprList: []
})

// 控制Dialog弹窗
const dialogVisible = ref(false)
const dialogTitle = ref('新增员工')


//职位列表数据
const jobs = ref([{ name: '班主任', value: 1 }, { name: '讲师', value: 2 }, { name: '学工主管', value: 3 }, { name: '教研主管', value: 4 }, { name: '咨询师', value: 5 }, { name: '其他', value: 6 }])
//性别列表数据
const genders = ref([{ name: '男', value: 1 }, { name: '女', value: 2 }])
//部门列表数据
const depts = ref([]);


//表单校验规则
const rules = ref({
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 2, max: 20, message: '用户名长度应在2到20个字符之间', trigger: 'blur' }
  ],
  name: [
    { required: true, message: '请输入姓名', trigger: 'blur' },
    { min: 2, max: 10, message: '姓名长度应在2到10个字符之间', trigger: 'blur' }
  ],
  gender: [
    { required: true, message: '请选择性别', trigger: 'change' }
  ],
  phone: [
    { required: true, message: '请输入手机号', trigger: 'blur' },
    { pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号', trigger: 'blur' }
  ]
});

// 表单校验动态数据
const empFormRef = ref();




// 钩子函数
onMounted(() => {
  search();
  searchDept();
})


// 查询
const search = async () => {
  const result = await queryAllApi(searchForm.value.name, searchForm.value.gender, searchForm.value.begin, searchForm.value.end, currentPage.value, pageSize.value);
  if (result.code) {
    empList.value = result.data.rows;
    total.value = result.data.total;
  }

}
// 清空
const clear = () => {
  searchForm.value = { name: '', gender: '', createTime: [], begin: '', end: '' };
  search();
}

// 查询部门信息
const searchDept = async () => {
  const result = await queryAllDeptApi();
  if (result.code) {
    depts.value = result.data;
  }
}

// 使用侦听searchForm.createTime变化时,将第一个值赋值给begin,第二个值赋值给end
watch(() => searchForm.value.createTime, (newVal, oldVal) => {
  if (newVal.length == 2) {
    searchForm.value.begin = newVal[0];
    searchForm.value.end = newVal[1];
  } else {
    searchForm.value.begin = '';
    searchForm.value.end = '';
  }
})



// 每页展示记录数变化时触发
const handleSizeChange = (size) => {
  search();
}
// 页码变化时触发
const handleCurrentChange = (pageSize) => {
  search();
}


// 新增员工
const addEmp = () => {
  dialogVisible.value = true;
  dialogTitle.value = '新增员工';
  employee.value = { username: '', name: '', gender: '', phone: '', job: '', salary: '', deptId: '', entryDate: '', image: '', exprList: [] }
}



//文件上传
// 图片上传成功后触发
const handleAvatarSuccess = (response) => {
  const result = response;
  if (result.code) {
    employee.value.image = result.data;
  }

}
// 文件上传之前触发
const beforeAvatarUpload = (rawFile) => {
  if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') {
    ElMessage.error('只支持上传图片')
    return false
  } else if (rawFile.size / 1024 / 1024 > 10) {
    ElMessage.error('只能上传10M以内图片')
    return false
  }
  return true
}






// 新增员工工作经历
const addEmpExpr = () => {
  employee.value.exprList.push({ "company": "", "job": "", "begin": "", "end": "", dataTime: [] })
}

// 删除员工工作经历
const deleteEmpExpr = (index) => {
  // splice 从第几个开始删除,删除几个
  employee.value.exprList.splice(index, 1);
}


// 侦听employee.exprList的变化,将exprList中的dataTime的值赋值给begin和end
watch(() => employee.value.exprList, (newVal, oldVal) => {
  if (employee.value.exprList && employee.value.exprList.length > 0) {
    employee.value.exprList.forEach((expr) => {
      expr.begin = expr.dataTime[0];
      expr.end = expr.dataTime[1];
    })
  }
}, { deep: true });


// 保存员工信息
const save = async () => {
  // 表单校验
  if (!empFormRef.value) return;
  empFormRef.value.validate(async (valid) => { // valid 表示是否校验通过
    if (valid) {  // 表单校验通过

      const result = await addApi(employee.value);
      if (result.code) {  // 成功
        ElMessage.success("保存成功")
        dialogVisible.value = false;
        search();
      } else {  // 失败
        ElMessage.error(result.msg);
      }

    } else { // 表单校验不通过
      ElMessage.error('表单校验不通过');

    }
  })
}



</script>

<template>
  <h1>员工管理</h1>

  <!-- 搜索框 -->
  <div class="container">
    <!-- {{ searchForm }} -->
    <el-form :inline="true">
      <el-form-item label="姓名">
        <el-input v-model="searchForm.name" placeholder="请输入员工姓名" clearable />
      </el-form-item>

      <el-form-item label="性别">
        <el-select v-model="searchForm.gender" placeholder="请选择" clearable>
          <el-option label="男" value="1" />
          <el-option label="女" value="2" />
        </el-select>
      </el-form-item>

      <el-form-item label="入职日期">
        <el-col>
          <el-form-item prop="date1">
            <el-date-picker v-model="searchForm.createTime" type="daterange" range-separator="To"
              start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" />
          </el-form-item>
        </el-col>
      </el-form-item>

      <el-form-item>
        <el-button type="primary" @click="search">查询</el-button>
        <el-button type="info" @click="clear">清空</el-button>
      </el-form-item>

    </el-form>
  </div>


  <!-- 功能按钮 -->
  <div class="container">
    <el-button type="primary" @click="addEmp">+ 新增员工</el-button>
    <el-button type="danger">- 批量删除</el-button>
  </div>

  <!-- 表格 -->
  <div class="container">
    <el-table :data="empList" border style="width: 100%">
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column prop="name" label="姓名" width="120" align="center" />
      <el-table-column label="性别" width="120" align="center">
        <template #default="scope">
          <span v-if="scope.row.gender == 1">男</span>
          <span v-else-if="scope.row.gender == 2">女</span>
          <span v-else>其他</span>
        </template>
      </el-table-column>
      <el-table-column label="头像" width="120" align="center">
        <template #default="scope">
          <!-- <img :src="scope.row.image" style="width: 50px; height: 50;" /> -->
          <el-avatar :src="scope.row.image" />
        </template>
      </el-table-column>
      <el-table-column prop="deptName" label="所属部门" width="120" align="center" />
      <el-table-column prop="job" label="职位" width="120" align="center">
        <template #default="scope">
          <span v-if="scope.row.job == 1">班主任</span>
          <span v-else-if="scope.row.job == 2">讲师</span>
          <span v-else-if="scope.row.job == 3">学工主管</span>
          <span v-else-if="scope.row.job == 4">教研主管</span>
          <span v-else-if="scope.row.job == 5">咨询师</span>
          <span v-else>其他</span>
        </template>
      </el-table-column>
      <el-table-column prop="entryDate" label="入职日期" width="180" align="center" />
      <el-table-column prop="updateTime" label="最后操作时间" width="200" align="center" />
      <el-table-column label="操作" align="center">
        <template #default="scope">
          <div class="mb-4">
            <!-- 点击编辑按钮,获取当前部门id参数给editDept方法 -->
            <el-button type="warning" text>
              <el-icon>
                <Edit />
              </el-icon>编辑
            </el-button>
            <el-button type="danger" text>
              <el-icon>
                <Delete />
              </el-icon>删除
            </el-button>
          </div>
        </template>
      </el-table-column>
    </el-table>
  </div>

  <!-- 分页条 -->
  <div class="demo-pagination-block" width="100%">
    <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[5, 10, 20, 50, 100]"
      :background=background layout="sizes, total, prev, pager, next, jumper" :total=total
      @size-change="handleSizeChange" @current-change="handleCurrentChange" />
  </div>



  <!-- 新增员工Dialog -->
  <el-dialog v-model="dialogVisible" :title="dialogTitle">
    <el-form :model="employee" label-width="80px" :rules="rules" ref="empFormRef">
      <!-- {{ employee }} -->
      <!-- 基本信息 -->
      <!-- 第一行 -->
      <!-- layout布局中 在行属性中配置gutter控制每列之间的距离,若不配置,默认值为0f -->
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="用户名" prop="username">
            <el-input v-model="employee.username" placeholder="请输入员工用户名,2-20个字"></el-input>
          </el-form-item>
        </el-col>

        <el-col :span="12">
          <el-form-item label="姓名" prop="name">
            <el-input v-model="employee.name" placeholder="请输入员工姓名,2-10个字"></el-input>
          </el-form-item>
        </el-col>
      </el-row>

      <!-- 第二行 -->
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="性别" prop="gender">
            <el-select v-model="employee.gender" placeholder="请选择性别" style="width: 100%;">
              <el-option v-for="g in genders" :key="g.value" :label="g.name" :value="g.value"></el-option>
            </el-select>
          </el-form-item>
        </el-col>

        <el-col :span="12">
          <el-form-item label="手机号" prop="phone">
            <el-input v-model="employee.phone" placeholder="请输入员工手机号"></el-input>
          </el-form-item>
        </el-col>
      </el-row>

      <!-- 第三行 -->
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="职位">
            <el-select v-model="employee.job" placeholder="请选择职位" style="width: 100%;">
              <el-option v-for="j in jobs" :key="j.value" :label="j.name" :value="j.value"></el-option>
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="薪资">
            <el-input v-model="employee.salary" placeholder="请输入员工薪资"></el-input>
          </el-form-item>
        </el-col>
      </el-row>

      <!-- 第四行 -->
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="所属部门">
            <el-select v-model="employee.deptId" placeholder="请选择部门" style="width: 100%;">
              <el-option v-for="d in depts" :key="d.id" :label="d.name" :value="d.id"></el-option>
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="入职日期">
            <el-date-picker v-model="employee.entryDate" type="date" style="width: 100%;" placeholder="选择日期"
              format="YYYY-MM-DD" value-format="YYYY-MM-DD"></el-date-picker>
          </el-form-item>
        </el-col>
      </el-row>

      <!-- 第五行 -->
      <el-row :gutter="20">
        <el-col :span="24">
          <el-form-item label="头像">
            <el-upload class="avatar-uploader" action="/api/upload" :show-file-list="false"
              :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
              <img v-if="employee.image" :src="employee.image" class="avatar" />
              <el-icon v-else class="avatar-uploader-icon">
                <Plus />
              </el-icon>
            </el-upload>
          </el-form-item>
        </el-col>
      </el-row>


      <!-- 工作经历 -->
      <!-- 第六行 -->
      <el-row :gutter="10">
        <el-col :span="24">
          <el-form-item label="工作经历">
            <el-button type="success" size="small" @click="addEmpExpr">+ 添加工作经历</el-button>
          </el-form-item>
        </el-col>
      </el-row>

      <!-- 第七行 ...  工作经历 -->
      <el-row :gutter="3" v-for="(expr, index) in employee.exprList">
        <el-col :span="10">
          <el-form-item size="small" label="时间" label-width="80px">
            <el-date-picker type="daterange" v-model="expr.dataTime" range-separator="至" start-placeholder="开始日期"
              end-placeholder="结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD"></el-date-picker>
          </el-form-item>
        </el-col>

        <el-col :span="6">
          <el-form-item size="small" label="公司" label-width="60px">
            <el-input placeholder="请输入公司名称" v-model="expr.company"></el-input>
          </el-form-item>
        </el-col>

        <el-col :span="6">
          <el-form-item size="small" label="职位" label-width="60px">
            <el-input placeholder="请输入职位" v-model="expr.job"></el-input>
          </el-form-item>
        </el-col>

        <el-col :span="2">
          <el-form-item size="small" label-width="0px">
            <el-button type="danger" @click="deleteEmpExpr(index)">- 删除</el-button>
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>

    <!-- 底部按钮 -->
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="save">保存</el-button>
      </span>
    </template>
  </el-dialog>

</template>

<style scoped>
.container {
  margin-top: 10px;
}

.avatar {
  height: 40px;
}

.avatar-uploader .avatar {
  width: 78px;
  height: 78px;
  display: block;
}

.avatar-uploader .el-upload {
  border: 1px dashed var(--el-border-color);
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: var(--el-transition-duration-fast);
}

.avatar-uploader .el-upload:hover {
  border-color: var(--el-color-primary);
}

.el-icon.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 78px;
  height: 78px;
  text-align: center;
  border-radius: 10px;
  /* 添加灰色的虚线边框 */
  border: 1px dashed var(--el-border-color);
}
</style>

13.修改员工

修改员工需要两步操作:

  1. 点击“编辑”按钮,执行根据ID查询员工信息,页面回显。
  2. 点击“保存”按钮,执行修改操作。

创建接口api

@/api/emp.js

// 根据id查询员工
export const queryByIdApi = (id) => request.get(`/emps/${id}`);

// 根据id修改员工信息
export const updateApi = (emp) => request.put("/emps",emp);

首先完成编辑打开Dialog对话框的基本实现

script头部导入import { queryAllApi, addApi, queryByIdApi, updateApi } from '../../api/emp';

在编辑按钮增加@click="edit(scope.row.id)",script增加下方代码

// 修改员工
const edit = async (id) => {

  const result = await queryByIdApi(id);
  if (result.code) {
    dialogVisible.value = true;
    dialogTitle.value = '修改员工';

    employee.value = result.data;
    // 对工作经历datTime进行单独处理
    let exprList = employee.value.exprList;
    if (exprList && exprList.length > 0) {
      exprList.forEach((expr) => {
        expr.dataTime = [expr.begin, expr.end];
      })
    }add
  }
}

接着完成修改员工功能

修改员工信息和新增员工信息使用的是同一Dialog对话框,不同的是新增员工不需要使用id参数,而修改员工需要提供员工id,通过判断动态数据employee是否含有id属性

点击save方法时,通过判断id值是进行updateApi还是``addApi`

// 保存员工信息
const save = async () => {
  // 表单校验
  if (!empFormRef.value) return;
  empFormRef.value.validate(async (valid) => { // valid 表示是否校验通过
    if (valid) {  // 表单校验通过


      let result;
      if (employee.value.id) { // 修改操作
        result = await updateApi(employee.value);
      } else { // 新增操作
        result = await addApi(employee.value);
      }


      if (result.code) {  // 成功
        ElMessage.success("保存成功")
        dialogVisible.value = false;
        search();
      } else {  // 失败
        ElMessage.error(result.msg);
      }
      
    } else { // 表单校验不通过
      ElMessage.error('表单校验不通过');

    }
  })
}

14.删除员工

  • 点击每条记录之后的“删除”按钮,删除当前这条记录;
  • 选择前面的复选框,选中要删除的员工,点击“批量删除”之后,会批量删除员工信息;

单个删除

创建删除员工api

// 根据id删除员工信息
export const deleteApi = (ids) => request.delete(`/emps?ids=${ids}`);

导入api,import { queryAllApi, addApi, queryByIdApi, updateApi, deleteApi } from '../../api/emp';

在删除按钮加上@click="deleteEmp(scope.row.id)"

最后编写deleteEmp方法

// 删除员工
const deleteEmp = (id) => {
  // 弹出确认框
  ElMessageBox.confirm(
    '你确定要删除该部门吗?',
    '提示',
    {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning',
    }
  ).then(async () => { // 确认
    const result = await deleteApi(id)
    if (result.code) { // 查询成功
      ElMessage.success('删除成功');
      search();
    } else { // 查询失败
      ElMessage.error(result.msg);
    }
  }).catch(() => { // 取消
    // ElMessage({type: 'info',message: '删除取消',})
    ElMessage.info('删除取消');
  })
}

批量删除

  • 为表格的复选框绑定时间,点击复选框之后,获取到目前选中的条件的id(对个id可以封装到数字中)。
  • 为“批量删除”按钮绑定时间,发送异步请求到服务端,根据id批量删除员工信息。

在表格组件标签加入@selection-change="handleSelectionChange属性,例如

<el-table :data="empList" border style="width: 100%" @selection-change="handleSelectionChange">

批量删除功能代码

// 记录勾选的员工的id
const selectedIds = ref([]);

// 表格复选框发生变化时触发 -- selection: 当前选中的数据(数组)
// const handleSelectionChange = (selection) => {
//   console.log(selection);
// }
const handleSelectionChange = (selection) => {
  selectedIds.value = selection.map(item => item.id);
}

// 批量删除员工
const deleteEmps = () => {
  // 弹出确认框
  ElMessageBox.confirm(
    '你确定要删除这些员工吗?',
    '提示',
    {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning',
    }
  ).then(async () => { // 确认
    if (selectedIds.value && selectedIds.value.length > 0) {
      const result = await deleteApi(selectedIds.value)
      if (result.code) { // 查询成功
        ElMessage.success('删除成功');
        search();
      } else { // 查询失败
        ElMessage.error(result.msg);
      }
    }else{
      ElMessage.info('您没有选择任何要删除的数据!');
    }

  }).catch(() => { // 取消
    // ElMessage({type: 'info',message: '删除取消',})
    ElMessage.info('删除取消');
  })
}

员工管理页面完成

<script setup>
import { ref, watch, onMounted } from 'vue';
import { queryAllApi, addApi, queryByIdApi, updateApi, deleteApi } from '../../api/emp';
import { queryAllApi as queryAllDeptApi } from "@/api/dept";
import { ElMessage, ElMessageBox } from 'element-plus';

// 搜索表单信息
const searchForm = ref({ name: '', gender: '', createTime: [], begin: '', end: '' });

// 员工信息
const empList = ref([])


// 分页条
const currentPage = ref(1)  // 页码
const pageSize = ref(10)    // 每页展示记录数
const background = ref(true)  //是否展示背景色
const total = ref(400)



//新增/修改表单
const employee = ref({
  username: '',
  name: '',
  gender: '',
  phone: '',
  job: '',
  salary: '',
  deptId: '',
  entryDate: '',
  image: '',
  exprList: []
})

// 控制Dialog弹窗
const dialogVisible = ref(false)
const dialogTitle = ref('新增员工')


//职位列表数据
const jobs = ref([{ name: '班主任', value: 1 }, { name: '讲师', value: 2 }, { name: '学工主管', value: 3 }, { name: '教研主管', value: 4 }, { name: '咨询师', value: 5 }, { name: '其他', value: 6 }])
//性别列表数据
const genders = ref([{ name: '男', value: 1 }, { name: '女', value: 2 }])
//部门列表数据
const depts = ref([]);


//表单校验规则
const rules = ref({
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 2, max: 20, message: '用户名长度应在2到20个字符之间', trigger: 'blur' }
  ],
  name: [
    { required: true, message: '请输入姓名', trigger: 'blur' },
    { min: 2, max: 10, message: '姓名长度应在2到10个字符之间', trigger: 'blur' }
  ],
  gender: [
    { required: true, message: '请选择性别', trigger: 'change' }
  ],
  phone: [
    { required: true, message: '请输入手机号', trigger: 'blur' },
    { pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号', trigger: 'blur' }
  ]
});

// 表单校验动态数据
const empFormRef = ref();




// 钩子函数
onMounted(() => {
  search();
  searchDept();
})


// 查询
const search = async () => {
  const result = await queryAllApi(searchForm.value.name, searchForm.value.gender, searchForm.value.begin, searchForm.value.end, currentPage.value, pageSize.value);
  if (result.code) {
    empList.value = result.data.rows;
    total.value = result.data.total;
  }

}
// 清空
const clear = () => {
  searchForm.value = { name: '', gender: '', createTime: [], begin: '', end: '' };
  search();
}

// 查询部门信息
const searchDept = async () => {
  const result = await queryAllDeptApi();
  if (result.code) {
    depts.value = result.data;
  }
}



// watch侦听--------------------演示------------------
// const a = ref(1);
// watch(a,(newVal,oldVal) => {
//   console.log(`更改前的值: ${oldVal},更改后的值: ${newVal}`)
// })


// // 深度侦听 设置 deep: true 
// // 侦听对象的全部属性
// const user = ref({ name: '', age: 1 });
// watch(user,(newVal,oldVale) => {  // 侦听user对象中的全部属性的变化
//   console.log(newVal);
// }, {deep: true})


// // 侦听对象的单个属性
// const user = ref({ name: '', age: 1 });
// watch(() => user.value.age,(newVal,oldVale) => {  // 侦听user对象中age属性的变化
//   console.log(`变化前的值: ${oldVale},变化后的值: ${newVal}`);
// })


// 使用侦听searchForm.createTime变化时,将第一个值赋值给begin,第二个值赋值给end
watch(() => searchForm.value.createTime, (newVal, oldVal) => {
  if (newVal.length == 2) {
    searchForm.value.begin = newVal[0];
    searchForm.value.end = newVal[1];
  } else {
    searchForm.value.begin = '';
    searchForm.value.end = '';
  }
})



// 每页展示记录数变化时触发
const handleSizeChange = (size) => {
  search();
}
// 页码变化时触发
const handleCurrentChange = (pageSize) => {
  search();
}


// 新增员工
const addEmp = () => {
  dialogVisible.value = true;
  dialogTitle.value = '新增员工';
  employee.value = { username: '', name: '', gender: '', phone: '', job: '', salary: '', deptId: '', entryDate: '', image: '', exprList: [] }

  // 重置表单校验规则--提示信息
  if (empFormRef.value) {
    empFormRef.value.resetFields();
  }
}



//文件上传
// 图片上传成功后触发
const handleAvatarSuccess = (response) => {
  const result = response;
  if (result.code) {
    employee.value.image = result.data;
  }

}
// 文件上传之前触发
const beforeAvatarUpload = (rawFile) => {
  if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') {
    ElMessage.error('只支持上传图片')
    return false
  } else if (rawFile.size / 1024 / 1024 > 10) {
    ElMessage.error('只能上传10M以内图片')
    return false
  }
  return true
}






// 新增员工工作经历
const addEmpExpr = () => {
  employee.value.exprList.push({ "company": "", "job": "", "begin": "", "end": "", dataTime: [] })
}

// 删除员工工作经历
const deleteEmpExpr = (index) => {
  // splice 从第几个开始删除,删除几个
  employee.value.exprList.splice(index, 1);
}


// 侦听employee.exprList的变化,将exprList中的dataTime的值赋值给begin和end
watch(() => employee.value.exprList, (newVal, oldVal) => {
  if (employee.value.exprList && employee.value.exprList.length > 0) {
    employee.value.exprList.forEach((expr) => {
      expr.begin = expr.dataTime[0];
      expr.end = expr.dataTime[1];
    })
  }
}, { deep: true });


// 保存员工信息
const save = async () => {
  // 表单校验
  if (!empFormRef.value) return;
  empFormRef.value.validate(async (valid) => { // valid 表示是否校验通过
    if (valid) {  // 表单校验通过


      let result;
      if (employee.value.id) { // 修改操作
        result = await updateApi(employee.value);
      } else { // 新增操作
        result = await addApi(employee.value);
      }


      if (result.code) {  // 成功
        ElMessage.success("保存成功")
        dialogVisible.value = false;
        search();
      } else {  // 失败
        ElMessage.error(result.msg);
      }

    } else { // 表单校验不通过
      ElMessage.error('表单校验不通过');

    }
  })
}


// 修改员工
const edit = async (id) => {

  const result = await queryByIdApi(id);
  if (result.code) {
    dialogVisible.value = true;
    dialogTitle.value = '修改员工';

    employee.value = result.data;
    // 对工作经历datTime进行单独处理
    let exprList = employee.value.exprList;
    if (exprList && exprList.length > 0) {
      exprList.forEach((expr) => {
        expr.dataTime = [expr.begin, expr.end];
      })
    }
  }
}


// 删除员工
const deleteEmp = (id) => {
  // 弹出确认框
  ElMessageBox.confirm(
    '你确定要删除该员工吗?',
    '提示',
    {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning',
    }
  ).then(async () => { // 确认
    const result = await deleteApi(id)
    if (result.code) { // 查询成功
      ElMessage.success('删除成功');
      search();
    } else { // 查询失败
      ElMessage.error(result.msg);
    }
  }).catch(() => { // 取消
    // ElMessage({type: 'info',message: '删除取消',})
    ElMessage.info('删除取消');
  })
}



// 记录勾选的员工的id
const selectedIds = ref([]);
// 表格复选框发生变化时触发 -- selection: 当前选中的数据(数组)
// const handleSelectionChange = (selection) => {
//   console.log(selection);
// }
const handleSelectionChange = (selection) => {
  selectedIds.value = selection.map(item => item.id);
}

// 批量删除员工
const deleteEmps = () => {
  // 弹出确认框
  ElMessageBox.confirm(
    '你确定要删除这些员工吗?',
    '提示',
    {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning',
    }
  ).then(async () => { // 确认
    if (selectedIds.value && selectedIds.value.length > 0) {
      const result = await deleteApi(selectedIds.value)
      if (result.code) { // 查询成功
        ElMessage.success('删除成功');
        search();
      } else { // 查询失败
        ElMessage.error(result.msg);
      }
    }else{
      ElMessage.info('您没有选择任何要删除的数据!');
    }

  }).catch(() => { // 取消
    // ElMessage({type: 'info',message: '删除取消',})
    ElMessage.info('删除取消');
  })
}


</script>

<template>
  <h1>员工管理</h1>
  <!-- {{ selectedIds }} -->

  <!-- 搜索框 -->
  <div class="container">
    <!-- {{ searchForm }} -->
    <el-form :inline="true">
      <el-form-item label="姓名">
        <el-input v-model="searchForm.name" placeholder="请输入员工姓名" clearable />
      </el-form-item>

      <el-form-item label="性别">
        <el-select v-model="searchForm.gender" placeholder="请选择" clearable>
          <el-option label="男" value="1" />
          <el-option label="女" value="2" />
        </el-select>
      </el-form-item>

      <el-form-item label="入职日期">
        <el-col>
          <el-form-item prop="date1">
            <el-date-picker v-model="searchForm.createTime" type="daterange" range-separator="To"
              start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" />
          </el-form-item>
        </el-col>
      </el-form-item>

      <el-form-item>
        <el-button type="primary" @click="search">查询</el-button>
        <el-button type="info" @click="clear">清空</el-button>
      </el-form-item>

    </el-form>
  </div>


  <!-- 功能按钮 -->
  <div class="container">
    <el-button type="primary" @click="addEmp">+ 新增员工</el-button>
    <el-button type="danger" @click="deleteEmps">- 批量删除</el-button>
  </div>

  <!-- 表格 -->
  <div class="container">
    <el-table :data="empList" border style="width: 100%" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column prop="name" label="姓名" width="120" align="center" />
      <el-table-column label="性别" width="120" align="center">
        <template #default="scope">
          <span v-if="scope.row.gender == 1">男</span>
          <span v-else-if="scope.row.gender == 2">女</span>
          <span v-else>其他</span>
        </template>
      </el-table-column>
      <el-table-column label="头像" width="120" align="center">
        <template #default="scope">
          <!-- <img :src="scope.row.image" style="width: 50px; height: 50;" /> -->
          <el-avatar :src="scope.row.image" />
        </template>
      </el-table-column>
      <el-table-column prop="deptName" label="所属部门" width="120" align="center" />
      <el-table-column prop="job" label="职位" width="120" align="center">
        <template #default="scope">
          <span v-if="scope.row.job == 1">班主任</span>
          <span v-else-if="scope.row.job == 2">讲师</span>
          <span v-else-if="scope.row.job == 3">学工主管</span>
          <span v-else-if="scope.row.job == 4">教研主管</span>
          <span v-else-if="scope.row.job == 5">咨询师</span>
          <span v-else>其他</span>
        </template>
      </el-table-column>
      <el-table-column prop="entryDate" label="入职日期" width="180" align="center" />
      <el-table-column prop="updateTime" label="最后操作时间" width="200" align="center" />
      <el-table-column label="操作" align="center">
        <template #default="scope">
          <div class="mb-4">
            <!-- 点击编辑按钮,获取当前部门id参数给editDept方法 -->
            <el-button type="warning" text @click="edit(scope.row.id)"><el-icon>
                <Edit />
              </el-icon>编辑</el-button>
            <el-button type="danger" text @click="deleteEmp(scope.row.id)"><el-icon>
                <Delete />
              </el-icon>删除</el-button>
          </div>
        </template>
      </el-table-column>
    </el-table>
  </div>

  <!-- 分页条 -->
  <div class="demo-pagination-block" width="100%">
    <el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize" :page-sizes="[5, 10, 20, 50, 100]"
      :background=background layout="sizes, total, prev, pager, next, jumper" :total=total
      @size-change="handleSizeChange" @current-change="handleCurrentChange" />
  </div>



  <!-- 新增员工Dialog -->
  <el-dialog v-model="dialogVisible" :title="dialogTitle">
    <el-form :model="employee" label-width="80px" :rules="rules" ref="empFormRef">
      <!-- {{ employee }} -->
      <!-- 基本信息 -->
      <!-- 第一行 -->
      <!-- layout布局中 在行属性中配置gutter控制每列之间的距离,若不配置,默认值为0f -->
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="用户名" prop="username">
            <el-input v-model="employee.username" placeholder="请输入员工用户名,2-20个字"></el-input>
          </el-form-item>
        </el-col>

        <el-col :span="12">
          <el-form-item label="姓名" prop="name">
            <el-input v-model="employee.name" placeholder="请输入员工姓名,2-10个字"></el-input>
          </el-form-item>
        </el-col>
      </el-row>

      <!-- 第二行 -->
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="性别" prop="gender">
            <el-select v-model="employee.gender" placeholder="请选择性别" style="width: 100%;">
              <el-option v-for="g in genders" :key="g.value" :label="g.name" :value="g.value"></el-option>
            </el-select>
          </el-form-item>
        </el-col>

        <el-col :span="12">
          <el-form-item label="手机号" prop="phone">
            <el-input v-model="employee.phone" placeholder="请输入员工手机号"></el-input>
          </el-form-item>
        </el-col>
      </el-row>

      <!-- 第三行 -->
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="职位">
            <el-select v-model="employee.job" placeholder="请选择职位" style="width: 100%;">
              <el-option v-for="j in jobs" :key="j.value" :label="j.name" :value="j.value"></el-option>
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="薪资">
            <el-input v-model="employee.salary" placeholder="请输入员工薪资"></el-input>
          </el-form-item>
        </el-col>
      </el-row>

      <!-- 第四行 -->
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="所属部门">
            <el-select v-model="employee.deptId" placeholder="请选择部门" style="width: 100%;">
              <el-option v-for="d in depts" :key="d.id" :label="d.name" :value="d.id"></el-option>
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="入职日期">
            <el-date-picker v-model="employee.entryDate" type="date" style="width: 100%;" placeholder="选择日期"
              format="YYYY-MM-DD" value-format="YYYY-MM-DD"></el-date-picker>
          </el-form-item>
        </el-col>
      </el-row>

      <!-- 第五行 -->
      <el-row :gutter="20">
        <el-col :span="24">
          <el-form-item label="头像">
            <el-upload class="avatar-uploader" action="/api/upload" :show-file-list="false"
              :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
              <img v-if="employee.image" :src="employee.image" class="avatar" />
              <el-icon v-else class="avatar-uploader-icon">
                <Plus />
              </el-icon>
            </el-upload>
          </el-form-item>
        </el-col>
      </el-row>


      <!-- 工作经历 -->
      <!-- 第六行 -->
      <el-row :gutter="10">
        <el-col :span="24">
          <el-form-item label="工作经历">
            <el-button type="success" size="small" @click="addEmpExpr">+ 添加工作经历</el-button>
          </el-form-item>
        </el-col>
      </el-row>

      <!-- 第七行 ...  工作经历 -->
      <el-row :gutter="3" v-for="(expr, index) in employee.exprList">
        <el-col :span="10">
          <el-form-item size="small" label="时间" label-width="80px">
            <el-date-picker type="daterange" v-model="expr.dataTime" range-separator="至" start-placeholder="开始日期"
              end-placeholder="结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD"></el-date-picker>
          </el-form-item>
        </el-col>

        <el-col :span="6">
          <el-form-item size="small" label="公司" label-width="60px">
            <el-input placeholder="请输入公司名称" v-model="expr.company"></el-input>
          </el-form-item>
        </el-col>

        <el-col :span="6">
          <el-form-item size="small" label="职位" label-width="60px">
            <el-input placeholder="请输入职位" v-model="expr.job"></el-input>
          </el-form-item>
        </el-col>

        <el-col :span="2">
          <el-form-item size="small" label-width="0px">
            <el-button type="danger" @click="deleteEmpExpr(index)">- 删除</el-button>
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>

    <!-- 底部按钮 -->
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="save">保存</el-button>
      </span>
    </template>
  </el-dialog>

</template>

<style scoped>
.container {
  margin-top: 10px;
}

.avatar {
  height: 40px;
}

.avatar-uploader .avatar {
  width: 78px;
  height: 78px;
  display: block;
}

.avatar-uploader .el-upload {
  border: 1px dashed var(--el-border-color);
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: var(--el-transition-duration-fast);
}

.avatar-uploader .el-upload:hover {
  border-color: var(--el-color-primary);
}

.el-icon.avatar-uploader-icon {
  font-size: 28px;
  color: #8c939d;
  width: 78px;
  height: 78px;
  text-align: center;
  border-radius: 10px;
  /* 添加灰色的虚线边框 */
  border: 1px dashed var(--el-border-color);
}
</style>

15.登录接口

实现基本的登录页面

@/views/login/index.vue

<script setup>
import { ref } from 'vue'
import { loginApi } from '@/api/login'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'

let loginForm = ref({ username: '', password: '' })

const router = useRouter();


const login = async () => {
  const result = await loginApi(loginForm.value)
  if(result.code){  // 登录成功
    // 1. 提示信息
    ElMessage.success('登录成功');
    // 2. 跳转页面 - 首页
    router.push('/');

  }else{  // 登录失败
    ElMessage.error(result.msg);
  }
}

const clear = () => {
  loginForm.value = { username: '', password: '' }
}
</script>

<template>
  <div id="container">
    <div class="login-form">
      <el-form label-width="80px">
        <p class="title">Tlias智能学习辅助系统</p>
        <el-form-item label="用户名" prop="username">
          <el-input v-model="loginForm.username" placeholder="请输入用户名"></el-input>
        </el-form-item>

        <el-form-item label="密码" prop="password">
          <el-input type="password" v-model="loginForm.password" placeholder="请输入密码"></el-input>
        </el-form-item>

        <el-form-item>
          <el-button class="button" type="primary" @click="login">登 录</el-button>
          <el-button class="button" type="info" @click="clear">重 置</el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>

<style scoped>
#container {
  padding: 10%;
  height: 410px;
  background-image: url('../../assets/bg1.jpg');
  background-repeat: no-repeat;
  background-size: cover;
}

.login-form {
  max-width: 400px;
  padding: 30px;
  margin: 0 auto;
  border: 1px solid #e0e0e0;
  border-radius: 10px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
  background-color: white;
}

.title {
  font-size: 30px;
  font-family: '楷体';
  text-align: center;
  margin-bottom: 30px;
  font-weight: bold;
}

.button {
  margin-top: 30px;
  width: 120px;
}
</style>

token存储问题

  • 需要在登录成功后,将令牌等信息存储起来。后续的请求中,再将令牌取出来,携带到服务器。

  • 有很多存储方式,例如cookie、sessionStorage和localstorage

LocalStorage是浏览器提供的本地存储机制(5MB)。

存储形式为key:value形式,键和值都是字符串类型。

API方法:

  • localStorage.setItem(key, value)
  • localStorage.getItem(key)
  • localStorage.removeItem(key)
  • localStorage.clear()

在登录方法加入一条代码即可

const login = async () => {
  const result = await loginApi(loginForm.value)
  if(result.code){  // 登录成功
    // 1. 提示信息
    ElMessage.success('登录成功');

    // 2. 存储token到localStorage
    localStorage.setItem('loginUser', JSON.stringify(result.data))

    // 3. 跳转页面 - 首页
    router.push('/');

  }else{  // 登录失败
    ElMessage.error(result.msg);
  }
}

携带令牌访问服务器

在后续的每一次Ajax请求中都获取localStorage中的令牌,在请求头中将令牌携带到服务端

axios提供了request拦截器response拦截器

  1. 首先拦截所有request请求
  2. localStorage获取token
  3. 判断token是否存在,若存在,在请求头加入token

@/utils/request.js

import axios from 'axios'

//创建axios实例对象
const request = axios.create({
  baseURL: '/api',
  timeout: 600000
})



//axios的请求 Request 拦截器 - 获取localStorage中的token,在请求头中增加token请求头
request.interceptors.request.use(
  (config) => { //成功回调
    //获取token
    const loginUser = JSON.parse(localStorage.getItem('loginUser'));  //将json字符串转换为对象
    //判断token是否存在
    if(loginUser && loginUser.token){
      // 在请求头中增加token值
      config.headers.token = loginUser.token;
    }
    return config;
  },
  (error) => { //失败回调
    return Promise.reject(error)
}
)


//axios的响应 response 拦截器
request.interceptors.response.use(
  (response) => { //成功回调
    return response.data
  },
  (error) => { //失败回调
    return Promise.reject(error)
  }
)

export default request

文件上传例外

在新增员工信息的头像上传功能,并没有使用我们自定义的axios,所以不会经过request拦截器,需要进行单独处理

  1. 在文件上传组件定义属性自定义请求头,动态获取token
    • 添加:headers="{'token': token}"
  2. 在页面加载时获取token绑定到文件上传组件

修改组件属性

<!-- 第五行 -->
      <el-row :gutter="20">
        <el-col :span="24">
          <el-form-item label="头像">
            <el-upload class="avatar-uploader" action="/api/upload" :show-file-list="false"
              :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload" :headers="{'token': token}">
              <img v-if="employee.image" :src="employee.image" class="avatar" />
              <el-icon v-else class="avatar-uploader-icon">
                <Plus />
              </el-icon>
            </el-upload>
          </el-form-item>
        </el-col>
      </el-row>

页面加载时获取token

// 文件上传绑定token
const token = ref();



// 钩子函数
onMounted(() => {
  search();
  searchDept();
  getToken();   // 获取token
})


// 获取token
const getToken = () => {
  const loginUser = JSON.parse(localStorage.getItem('loginUser'))
  if(loginUser && loginUser.token){
    token.value = loginUser.token;
  }
}

响应401跳转登录页面

使用axiosresponse拦截器实现401跳转登录页面

@/utils/request.js

import axios from 'axios'
import { ElMessage } from 'element-plus';
import router from '@/router/index'

//创建axios实例对象
const request = axios.create({
  baseURL: '/api',
  timeout: 600000
})



//axios的请求 Request 拦截器 - 获取localStorage中的token,在请求头中增加token请求头
request.interceptors.request.use(
  (config) => { //成功回调
    //获取token
    const loginUser = JSON.parse(localStorage.getItem('loginUser'));  //将json字符串转换为对象
    //判断token是否存在
    if(loginUser && loginUser.token){
      // 在请求头中增加token值
      config.headers.token = loginUser.token;
    }
    return config;
  },  
  (error) => { //失败回调
    return Promise.reject(error)
}
)


//axios的响应 response 拦截器
request.interceptors.response.use(
  (response) => { //成功回调
    return response.data
  },
  (error) => { //失败回调
    // console.log(error)
    if(error.response.status === 401){
      // 提示信息
      ElMessage.error('登录超时,请先登录');
      // 跳转到登录页面
      router.push('/login');
    }else{
      ElMessage.error('接口访问异常');
    }
    return Promise.reject(error)
  }
)

export default request

退出登录按钮

  1. 展示当前登录员工
  2. 实现退出登录功能(跳转到登录页面)

@/views/layout/index.vue

<script setup>
import { ref, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'
import router from '@/router/index';

const name = ref('');

onMounted(() => {
  const loginUser = JSON.parse(localStorage.getItem('loginUser'));
  if (loginUser && loginUser.name) {
    name.value = loginUser.name
  }
});



// 退出登录
const logout = () => {
  // 弹出确认框
  ElMessageBox.confirm(
    '你确定退出登录吗?',
    '提示',
    {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning',
    }
  ).then(async () => { // 确认
    // 1.提示信息
    ElMessage.success('退出登录成功');
    // 2.清空本地存储
    localStorage.removeItem('loginUser');
    // 3.跳转到登录页面
    router.push('/login');

  }).catch(() => { // 取消
    ElMessage.info('您已取消退出');
  })
}

</script>

<template>
  <div class="common-layout">
    <el-container>
      <!-- Header 区域 -->
      <el-header class="header">
        <span class="title">Tlias智能学习辅助系统</span>
        <span class="right_tool">
          <a href="">
            <el-icon>
              <EditPen />
            </el-icon> 修改密码 &nbsp;&nbsp;&nbsp; | &nbsp;&nbsp;&nbsp;
          </a>
          <!-- 将href变为死链接,点击不会刷新跳转页面 -->
          <a href="javascript:void(0)" @click="logout">
            <el-icon>
              <SwitchButton />
            </el-icon> 退出登录 【{{ name }}】
          </a>
        </span>
      </el-header>
        
      <!-- 左侧菜单 -->

      <!-- 核心展示区 -->
        
</template>

16.打包部署

在前后端项目中,前端和后端的项目部署都是分离的,现在前端项目的部署,通常使用nginx服务器进行部署

打包

在现在流行的vite框架中,使用npm run build即可完成项目的打包,会在当前项目生成一个dist目录,存放打包后的文件

npm run build

部署(nginx)

Nginx是一款轻量级的Web服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。其特点是占有内存少,并发能力强,在各大互联网公司都有非常广泛的使用。

目录结构

路径 描述
D:\nginx\ 根目录,包含所有 NGINX 运行相关文件
D:\nginx\nginx.exe 主程序,可执行文件
D:\nginx\conf\ 配置文件目录
D:\nginx\conf\nginx.conf 主配置文件
D:\nginx\conf\mime.types MIME 类型定义文件
D:\nginx\html\ 默认网站根目录
D:\nginx\html\index.html 默认欢迎页面
D:\nginx\logs\ 日志目录
D:\nginx\logs\access.log 访问日志
D:\nginx\logs\error.log 错误日志
D:\nginx\temp\(可选) 临时文件目录,可能需手动创建(用于上传等)

下载并解压缩nginx预编译版本之后,只需要将打包的dist目录下的内容拷贝到D:\nginx\html\即可

配置好以后,只需要运行D:\nginx\nginx.exe即可

问题:访问后接口访问异常,是因为在vite项目中使用了代理访问api目录转发到tomcat后端服务器

解决方案: Nginx反向代理

Nginx反向代理

实现以下功能:

用户访问:

http://localhost/api/emps

NGINX 自动代理到:

http://localhost:8080/emps

配置文件如下:

D:\nginx\conf\nginx.conf,在server配置向下加入

server {
        listen       80;		// 默认
        server_name  localhost;	// 默认

        location /api/ {
        proxy_pass http://localhost:8080/;
        rewrite ^/api/(.*)$ /$1 break;
        }
指令 说明
location /api/ 匹配以 /api/ 开头的请求
proxy_pass http://localhost:8080/; 转发请求到 Tomcat 服务器
rewrite ^/api/(.*)$ /$1 break; 移除 /api/ 前缀,否则会传给 Tomcat 变成 /api/emps

修改配置文件,任务管理其找到nginx进程关闭,接着启动nginx服务器

当然更简便的是使用D:\nginx\nginx.exe -s reload,重启nginx服务器

D:\nginx\nginx.exe -s stop,关闭nginx服务器

那么恭喜你,完成了前端项目的开发、打包和部署。

posted @ 2025-06-08 11:44  Junglezt  阅读(81)  评论(0)    收藏  举报