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 install 或 yarn 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.组件关系
- 在使用浏览器访问主页的时候,默认会访问
index.html
- 在
index.html
中,通常使用<script src="/src/main.js</script>
引用了main.js
文件并且<div id="app">
绑定了app
- 在
main.js
中通过vue
导入了createApp
方法,通过createApp(App).mount(#app)
将App.vue
组件挂载到#app
锚点 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> 修改密码 |
</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.vue
的index
和router
属性
<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> 修改密码 |
</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.部门管理列表
- 首先使用
element-plus
官网查找按钮和表格,完成页面的基本设计 - 实现搜索功能,页面加载使用钩子方法自动加载
@/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
-
首先在
src
目录创建utils
和api
目录 -
接着在
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
-
然后在
api
目录中创建dept.js
,内容如下import request from '@/utils/request' // 查询全部部门数据 // export const queryAllApi = () => { // return request.get('/depts'); // } // 可以省略return export const queryAllApi = () => request.get('/depts');
api
目录主要负责前端与后端交互的部分
-
修改部门管理页面使用优化有的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.新增部门
- 点击新增部门打开
form
表单 - 输入新增部门信息,点击保存按钮调用新增部门接口
@/api/dept.js
// 新增部门
export const addApi = (dept) => request.post('/depts', dept);
导入api
// 导入api
import { queryAllApi, addApi, queryByIdApi } from '@/api/dept'
新增员工Dialog
对话框
-
在新增部门按钮添加
@click="addDept"
属性 -
添加
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.修改部门
-
点击编辑打开编辑部门
form
表单(含有当前部门信息)逻辑实现: 调用根据id查询部门信息接口
-
点击保存修改部门信息
逻辑实现:调用修改部门信息接口
*@/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
表示获取当前行的索引
逻辑分析:
- 用户点击编辑按钮后,会根据当前
id
获取部门信息并返回 - 用户修改完数据,点击保存更改用户信息
注: 当前修改部门和新增部门使用的是同一个
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.员工页面布局
搜索表单
- 首先在
element-plus
官网找到表单组件,并设计搜索表单 - 根据搜索表单设计双向数据绑定
searchForm
- 写好
search
和clear
按钮的基础功能
@/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侦听
作用: 侦听一个或多个响应式数据源,并在数据源发生变化时调用传入的回调函数。
用法:
- 导入watch函数
- 执行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>
添加、删除工作经历
功能实现:
- 每次点击添加工作经历,增加一个工作经历信息
- 点击删除,删除指定的工作经历信息
代码流程如下:
- 在添加员工工作经历按钮标签增加属性
@click="addEmpExpr"
- 编写
addEmpExpr
方法代码 - 使用
v-for
配合v-model
优化员工多条工作经历显示 - 在删除员工工作经历按钮标签增加属性
@click="deleteEmpExpr(index)"
,获取循环次数的index
参数,方便删除 - 使用
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>
新增员工功能实现
- 点击保存,发送异步请求到服务器,提交数据。
- 保存完毕后,如果成功,关闭对话框,重新加载列表数据。
- 保存完毕后,如果失败,提示错误信息。
@/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>
新增员工表单校验
表单校验流程:
- 首先定义校验规则
- 在
form
表单引用规则- 表单
rules
属性,input
等标签使用prop
选择校验规则
- 表单
- 在表单提交时进行表单校验
- 表单
ref
属性,通过判断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();
接着在表单使用改规则,注意表单的rules
、ref
属性和内部文本框、复选框内的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.修改员工
修改员工需要两步操作:
- 点击“编辑”按钮,执行根据ID查询员工信息,页面回显。
- 点击“保存”按钮,执行修改操作。
创建接口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拦截器
- 首先拦截所有
request
请求 - 从
localStorage
获取token - 判断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
拦截器,需要进行单独处理
- 在文件上传组件定义属性自定义请求头,动态获取
token
- 添加
:headers="{'token': token}"
- 添加
- 在页面加载时获取
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跳转登录页面
使用axios
的response
拦截器实现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
退出登录按钮
- 展示当前登录员工
- 实现退出登录功能(跳转到登录页面)
@/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> 修改密码 |
</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服务器
那么恭喜你,完成了前端项目的开发、打包和部署。