01-前端 Web 开发(HTML+CSS)

初识 Web 前端

介绍

我们介绍 Web 网站工作流程的时候提到,前端开发,主要的职责就是将数据以好看的样式呈现出来。说白了,就是开发网页程序,如下图所示:

主要明确以下三个问题:

1). 网页由哪些部分组成 ?

  • 文字、图片、音频、视频、超链接、表格等等。

2). 我们看到的网页,背后的本质是什么 ?

  • 前端程序员写的前端代码 (备注:在前后端分离的开发模式中)

3). 前端的代码是如何转换成用户眼中的网页的 ?

  • 通过浏览器转化(解析和渲染)成用户看到的网页
  • 浏览器中对代码进行解析和渲染的部分,称为 浏览器内核

而市面上的浏览器非常多,比如:IE、火狐 Firefox、苹果 safari、欧朋、谷歌 Chrome、QQ 浏览器、360 浏览器等等。 而且我们电脑上安装的浏览器可能都不止一个,有很多。

但是呢,需要大家注意的是,不同的浏览器,内核不同,对于相同的前端代码解析的效果也会存在差异。 那这就会造成一个问题,同一段前端程序,不同浏览器展示出来的效果是不一样的,这个用户体验就很差了。而我们想达到的效果则是,即使用户使用的是不同的浏览器,解析同一段前端代码,最终展示出来的效果都是相同的。

要想达成这样一个目标,我们就需要定义一个统一的标准,然后让各大浏览器厂商都参照这个标准来实现即可。 而这套标准呢,其实早都已经定义好了,那就是我们接下来,要介绍的 web 标准

Web 标准

Web 标准也称为网页标准,由一系列的标准组成,大部分由 W3C( World Wide Web Consortium,万维网联盟)负责制定。由三个组成部分:

  • HTML:负责网页的结构(页面元素和内容)。
  • CSS:负责网页的表现(页面元素的外观、位置等页面样式,如:颜色、大小等)。
  • JavaScript:负责网页的行为(交互效果)。

那在我们的前端课程中,除了会讲解 HTML、CSS、JS 这些技术以外,还会讲解现在前端开发中的高级技术 Vue、ElementPlus、Axios。

而 Web 前端开发的内容呢,我们在设计的时候,也进行了分层的设计,分为了两个部分:

  • Web 前端基础:HTML、CSS、JS、Vue3、Ajax、Axios,前端基础部分为两天时间。
  • Web 前端进阶:Vue 工程化、ElementPlus、Tlias 智能学习辅助系统案例,前端进阶部分为三天时间。

那今天我们就先来讲解 Web 前端基础的第一部分: HTML & CSS。

[!TIP]
什么是 HTML?
HTML: HyperText Markup Language,超文本标记语言。

  • 超文本:超越了文本的限制,比普通文本更强大。除了文字信息,还可以定义图片、音频、视频等内容。

  • 标记语言:由标签 “< 标签名 >” 构成的语言

    • HTML 标签都是预定义好的,例如:使用 <h1> 标签展示标题,使用 <a> 展示超链接,使用 <img> 展示图片,<video> 展示视频。
    • HTML 代码直接在浏览器中运行,HTML 标签由浏览器解析 。
  • 下面展示的是一段 html 代码经过浏览器解析,呈现的效果如右图所示:

[!TIP]
什么是 CSS?

  • CSS: Cascading Style Sheet,层叠样式表,用于控制页面的样式(表现)。

下面展示的是一段 html 代码 及 CSS 样式 经过浏览器解析,呈现的效果如右图所示:

HTML 快速入门

操作步骤

  1. 新建文本文件,后缀名改为 .html,命名为:01. html 快速入门.html

创建一个名为 HTML 的文件夹,然后找到课程资料中的 1.png 文件放到该目录下的 img 目录中。此时 HTML 文件夹中内容如下:

  1. 写 HTML 的基本骨架,定义标题

选中文件,鼠标右击,选择使用记事本打开文件,并且编写网页的标题。

首先 html 有固定的基本结构

1
2
3
4
5
6
7
8
<html>
<head>
<title>HTML 快速入门</title>
</head>
<body>

</body>
</html>

其中 <html> 是根标签,<head><body> 是子标签。

  • <head> : 定义网页的头部,用来存放给浏览器看的信息,如:CSS 样式、网页的标题。
  • <body> : 定义网页的主体部分,存放给用户看的信息,也是网页的主体内容,如:文字、图片、视频、音频、表格等。
  1. 在中编写 HTML 的核心内容
1
2
3
4
5
6
7
8
9
<html>
<head>
<title>HTML 快速入门</title>
</head>
<body>
<h1>Hello HTML</h1>
<img src="./static/1.png">
</body>
</html>

其中 <h1> 标签是一个一级标题的标签。 <img> 标签是一个图片标签,用来展示图片,而其中的 src 属性是用来指定要展示的图片。

  1. 然后选中文件,鼠标右击,选择使用浏览器打开文件

浏览器呈现效果如下:

2.2 总结

1). HTML 页面的基础结构标签

1
2
3
4
5
6
7
8
<html>
<head>
<title> </title>
</head>
<body>

</body>
</html>

<title> 中定义标题显示在浏览器的标题位置,<body> 中定义的内容会呈现在浏览器的内容区域

2). HTML 中的标签特点

  • HTML 标签不区分大小写,建议小写
  • HTML 标签的属性值,采用单引号、双引号都可以,一般写双引号
  • HTML 语法相对比较松散 (建议大家编写 HTML 标签的时候尽量严谨一些)

前端开发工具

我们通过快速入门案例,发现由记事本文件开发 html 是非常不方便的,所以接下来我们需要学习一款前端专业的开发工具 VS Code。

Visual Studio Code(简称 VS Code )是 Microsoft 于 2015 年 4 月发布的一款代码编辑器。VS Code 对前端代码有非常强大的支持,同时也其他编程语言(例如:C++、Java、Python、PHP、Go 等)。VS Code 提供了非常强大的插件库,大大提高了开发效率。

  • 官网: https://code.visualstudio.com

  • 安装:VsCode 安装文档

  • 注意:

    • 需要注意的是,我们作为一名开发者,不应该将软件软装在包含中文名的路径中 。
    • 由于安装了 IDEA 快捷键的插件,VSCode 快键键与 IDEA 是一致的。

常见标签&样式

那我们在讲解 HTML 的常见基础标签 及 CSS 的基本样式时,我们就以 新浪新闻页面 为例,来进行讲解,这样大家不仅能够知道 常见标签及样式的作用,还能够知道具体的应用场景。

央视新闻的具体页面效果如下:

原始页面网址:https://news.cctv.com/2024/05/15/ARTIflTnFvNMx9nUVc4PA7T2240515.shtml

而大家可以看到,上述新闻网页,其实分为两个部分,一个是新闻的标题部分,另一个是新闻的正文部分。那接下来,我们就先来完成央视新闻标题部分的制作。

央视新闻-标题

前面我们提到,我们在浏览器中看到的网页程序呈现出来的效果,实际上是浏览器解析并渲染了前端代码而呈现出来的。 而我们所编写的 HTML 页面,在浏览器中渲染的时候,是从上往下逐行解析展示的。 所以,我们在编写 html 页面的时候,要根据页面的布局,从上往下编写。

标题排版

在制作网页的时候,我们可以充分的利用 AI 辅助工具通义灵码,来帮我们实现对应的功能,我们只需要在编辑器中给定对应的注释(提示词),通义灵码就可以帮我们实现对应的功能效果,然后我们再基于生成的代码进行调试即可。

接下来,我们就可以通过 VsCode 打开 HTML 文件夹,然后在其中创建一个 html 页面,命名为:02. 央视新闻-标题排版.html

然后在这个文件中来制作新浪新闻网页,标题部分的排版内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章</title>
</head>
<body>

_ _<!-- 定义网页标题, 标题内容: 【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章 -->
<h1 id="title">【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章</h1>

<!-- 定义一个超链接, 链接地址:https://news.cctv.com/, 链接内容:央视网 -->
<a href="https://news.cctv.com/" target="_blank">央视网</a>
2024年05月15日 20:07

</body>
</html>

那在上述我们用到的两个标签,一个是标题标签 <h1></h1>,另一个是超链接标题 <a></a>。这两个标签的具体用法如下:

[!TIP]
标题标签 h 系列:

11111111111111

11111111111111

11111111111111

11111111111111

11111111111111
11111111111111

**效果:**h1 为一级标题,字体也是最大的 ; h6 为六级标题,字体是最小的。
**注意:**HTML 标签是预定义好的,不能随意定义,也就以为着,标题标签就只有这六个,没有

[!TIP]
超链接 a 标签:
标签:央视网
属性:

标题样式

我们可以看到,目前我们制作的新闻标题部分,新闻发布时间 2024年05月15日 20:07 的字体颜色是黑色,而在原始的央视新闻页面中,字体的颜色呈现灰色,具体的呈现效果如下:

那接下来,我们要来控制字体的颜色,而这部分其实是属于网页的样式,所以这里呢,就要通过 CSS 样式控制

这些基础的功能我们就可以直接通过 AI 工具帮我们实现,参考如下提示词(prompt):

[!TIP]
你是一名前端开发工程师,请通过 css 为网页中的新闻发布时间设置字体颜色为灰色。 网页内容如下:

【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章

AI 给出的最终代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章</title>
<style>
.publish-date {
color: gray;
}
</style>
</head>
<body>

<!-- 定义网页标题, 标题内容: 【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章 -->
<h1 id="title">【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章</h1>

<!-- 定义一个超链接, 链接地址:https://news.cctv.com/, 链接内容:央视网 -->
<a href="https://news.cctv.com/" target="_blank">央视网</a>
<span class="publish-date">2024年05月15日 20:07</span>

</body>
</html>

我们可以看到,通过上面这样一段代码,就可以控制字体的颜色,而其实上面这一段代码就是 AI 生成的 CSS 样式。最终效果如下:

CSS 引入方式

那在 HTML 的文件中,我们如何来编写 CSS 样式呢,此时就涉及到 CSS 的三种引入方式。

具体有 3 种引入方式,语法如下表格所示:

对于上述 3 种引入方式,企业开发的使用情况如下:

  • **行内样式:**会出现大量的代码冗余,不方便后期的维护,所以不常用(常配合 JS 使用)。
  • 内部样式**:**通过定义 css 选择器,让样式作用于当前页面的指定的标签上。(可以写在页面任何位置,但通常约定写在 head 标签中)
  • **外部样式:**html 和 css 实现了完全的分离,企业开发常用方式。
颜色表示方式

在前端程序开发中,颜色的表示方式常见的有如下三种:

设置字体颜色
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章</title>
<!-- 2. 内部样式 -->
<style>
.publish-date {
color: #b2b2b2;
}
</style>
<!-- 3. 外部样式 -->
<!-- <link rel="stylesheet" href="css/news.css"> -->
</head>
<body>

<!-- 定义网页标题, 标题内容: 【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章 -->
<h1 id="title">【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章</h1>

<!-- 定义一个超链接, 链接地址:https://news.cctv.com/, 链接内容:央视网 -->
<a href="https://news.cctv.com/" target="_blank">央视网</a>

<!-- 1. 行内样式 -->
<!-- <span style="color: #b2b2b2;">2024年05月15日 20:07</span> -->

<span class="publish-date">2024年05月15日 20:07</span>

</body>
</html>

备注: 要想拾取某一个网页中的颜色,我们可以借助于截图软件的拾色器插件来完成。【截图软件在资料中已经提供】

CSS 选择器

顾名思义:选择器是选取需设置样式的元素(标签),但是我们根据业务场景不同,选择的标签的需求也是多种多样的,所以选择器有很多种。

选择器通用语法如下

1
2
3
4
**选择器名**   {
css样式名:css样式值;
css样式名:css样式值;
}

而我们是做后台开发的,所以对于 css 选择器,我们只学习常见的这几种:

那接下来,我们需要将页面上所有的超链接中,默认的下划线效果去除掉。具体的代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章</title>
<!-- 2. 内部样式 -->
<style>
.publish-date {
color: #b2b2b2;
}

/* 设置超链接取消下划线效果 */
a {
text-decoration: none;
}
</style>
<!-- 3. 外部样式 -->
<!-- <link rel="stylesheet" href="css/news.css"> -->
</head>
<body>

<!-- 定义网页标题, 标题内容: 【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章 -->
<h1 id="title">【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章</h1>

<!-- 定义一个超链接, 链接地址:https://news.cctv.com/, 链接内容:央视网 -->
<a href="https://news.cctv.com/" target="_blank">央视网</a>

<!-- 1. 行内样式 -->
<!-- <span style="color: #b2b2b2;">2024年05月15日 20:07</span> -->

<span class="publish-date">2024年05月15日 20:07</span>

</body>
</html>

那到目前为止,标题部分的基本排版和样式,我们就已经制作完成了,具体页面效果:

央视新闻-正文

在原始的央视新闻页面中,我们可以看到有视频,有文字,有图片,内容还是非常丰富的。

正文排版

基本实现

浏览器在解析渲染页面的时候,是从上往下解析渲染的,那接下来,我们就可以从上往下来布局这个页面,那这个过程中,我们就可以直接基于 AI 来辅助我们实现。

正文排版之后,页面的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章</title>
<!-- 2. 内部样式 -->
<style>
.publish-date {
color: #b2b2b2;
}

/* 设置超链接取消下划线效果 */
a {
text-decoration: none;
}
</style>
</head>
<body>

<!-- 定义网页标题, 标题内容: 【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章 -->
<h1 id="title">【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章</h1>

<!-- 定义一个超链接, 链接地址:https://news.cctv.com/, 链接内容:央视网 -->
<a href="https://news.cctv.com/" target="_blank">央视网</a>

<span class="publish-date">2024年05月15日 20:07</span>
<br>
<br>

<!-- 定义一个视频, video/news.mp4 -->
<video src="video/news.mp4" controls width="80%"></video>
<p>
央视网消息(新闻联播):作为共抓长江大保护的标志性工程,长江十年禁渔今年进入第四年。总书记指出,长江禁渔是为全局计、为子孙谋的重要决策。牢记总书记嘱托,沿江省市持续推进长江水生生物多样性恢复,努力保障退捕渔民就业生活。这段时间,记者深入长江两岸,记录下禁渔工作取得的重要阶段性成效和广大干部群众坚定不移推进长江十年禁渔的扎实行动。
</p>
<p>
行走在长江沿线,科研人员发现很多可喜现象。
</p>
<!-- 定义一张图片, img/1.gif -->
<img src="./static/1.gif" alt="" width="100%">
<p>
在长江南源,一处小头裸裂尻鱼新的栖息地被发现,鱼的数量大约超3万尾,为水生态保护提供了珍贵数据。
</p>
<p>
在长江中游,追踪显示,人工增殖放流的中华鲟成功入海率已经从45%左右提升至60%以上;鄱阳湖鱼类小型化、低龄化趋势得到遏制,栖息地生存环境得以改善。
</p>
<p>
在长江下游,今年3月起,南京秦淮河入江口首次出现野生中华绒螯蟹大规模洄游现象,种群数量明显增加。
</p>
<img src="./static/2.jpg" width="100%">
<p>
水生生物资源恢复向好,见证了长江十年禁渔三年多来的阶段性成果。
</p>
<p>
实施长江十年禁渔,是以同志为核心的党中央从中华民族长远利益出发作出的重要决策。党的十八大以来,总书记多次深入长江沿线考察调研,详细了解长江十年禁渔的实施情况,他指出,要坚定推进长江十年禁渔,巩固好已经取得的成果。
</p>
<img src="./static/3.jpg" width="100%">
<p>
按照部署,自2021年1月1日起,在长江干流、大型通江湖泊、重要支流和长江口部分海域实行为期十年的禁渔,常年禁止天然渔业资源的生产性捕捞。禁渔三年多来,相关评估显示,长江干流和鄱阳湖、洞庭湖水生生物完整性指数由禁渔前最差的“无鱼”提升了两个等级。2022年,长江江豚数量达到1249头,实现历史性止跌回升。长江干流水质连续4年全线保持Ⅱ类。
</p>
<p>
实施长江十年禁渔,解决好渔民上岸后的生产生活问题,禁渔才有稳定扎实的社会基础。
</p>
<img src="./static/4.jpg" width="100%">
<p>
安徽退捕转产的3万多名渔民,在政府的引导下接受就业培训。在当涂县,免费学习养殖技术,养殖生态螃蟹成了退捕渔民的新选择。
</p>
<p>
在拥有洞庭湖超六成水域的湖南岳阳,政府帮扶上岸渔民建起养殖场,发展风干鱼产业,还带领他们学习直播带货,拓宽销路。
</p>
<p>
在渔民退捕上岸的鄱阳湖棠荫岛,当地在继续保护好生态的前提下,正探索规划利用独特的自然资源发展旅游产业。禁渔三年多来,有关部门对23.1万退捕渔民逐一建档立卡,多渠道提升就业、社保水平。
</p>
<img src="./static/5.jpg" width="100%">
<p>
长江十年禁渔实施以来,沿江省市合力攻坚、久久为功,长江大保护不断向纵深推进,持续巩固禁渔成果。下一步,沿江省市还将加强水生生物重要栖息地修复,建立退捕渔民动态精准帮扶服务,完善跨区域、跨部门执法合作机制,确保一江清水绵延后世、惠泽人民。
</p>
<img src="./static/6.jpg">

</body>
</html>

注意:在使用类似于通义零码这类的 AI 辅助工具时,如果存在一些敏感数据,比如:政治、宗教、信仰等。

常见标签

那在上述的正文排版中,用到了如下标签,接下来详解介绍一下:

在 HTML 页面中,我们在代码中录入空格、<、> 这些符号的时候,是没有对应的效果的,因为浏览器并不能准确的识别,此时,我们就需要通过字符实体来表示空格,<, > 。常见符号的字符实体如下:

路径表示

在引入图片、视频、音频、css 等内容时,我们需要指定文件的路径,而在前端开发中,路径的书写形式分为两类:

  • 绝对路径:

    • 绝对磁盘路径: <img src="/2025/06/04/前端 Web 开发/static" >
    • 绝对网络路径: <img src="/2025/06/04/前端 Web 开发/" >
  • 相对路径:

    1
    2
    3
    ./ : 当前目录 , ./ 可以省略的

    ../: 上一级目录

正文样式

正文的基本排版有了之后,接下来,我们要处理的是正文部分的样式。

具体的 css 样式如下:

1
2
3
4
5
/* 设置段落首行缩进 */
p {
text-indent: 2em; /* 首行缩进2em */
line-height: 2; /* 行高2倍 */
}

央视新闻-布局

功能实现

完成了标签及正文部分的排版制作,以及样式处理之后,那最后一步,我们就要来完成页面整体布局的设置了。 从原始的央视新闻页面中,我们可以看到,新闻页面是出于整个版面的正中心的,那这种呢,在布局中也称为 版心居中

这个功能,我们依然可以通过 AI,轻而易举的实现。具体的提示词如下:

[!TIP]
通过 css 使如下新闻网页的整体内容,占用整个页面宽度的 70%,并且横向居中展示。具体的网页内容如下:…

最终网页代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章</title>
<style>
.publish-date {
color: #b2b2b2;
}

/* 设置超链接取消下划线效果 */
a {
text-decoration: none;
}

/* 设置段落首行缩进 */
p {
text-indent: 2em; /* 首行缩进2em */
line-height: 2; /* 行高2倍 */
}

/* 新增样式 */
.news-content {
width: 70%; /* 宽度占70% */
margin: 0 auto; /* 横向居中 */
}
</style>
</head>
<body>
<!-- 包裹新闻内容的容器 -->
<div class="news-content">
<!-- 定义网页标题, 标题内容: 【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章 -->
<h1 id="title">【新思想引领新征程】推进长江十年禁渔 谱写长江大保护新篇章</h1>

<!-- 定义一个超链接, 链接地址:https://news.cctv.com/, 链接内容:央视网 -->
<a href="https://news.cctv.com/" target="_blank" >央视网</a>

<span class="publish-date">2024年05月15日 20:07</span>
<br>
<br>

<!-- 定义一个视频, video/news.mp4 -->
<video src="video/news.mp4" controls width="100%"></video>
<p>
<strong>央视网消息</strong>(新闻联播):作为共抓长江大保护的标志性工程,长江十年禁渔今年进入第四年。总书记指出,长江禁渔是为全局计、为子孙谋的重要决策。牢记总书记嘱托,沿江省市持续推进长江水生生物多样性恢复,努力保障退捕渔民就业生活。这段时间,记者深入长江两岸,记录下禁渔工作取得的重要阶段性成效和广大干部群众坚定不移推进长江十年禁渔的扎实行动。
</p>
<p>
行走在长江沿线,科研人员发现很多可喜现象。
</p>
<!-- 定义一张图片, img/1.gif -->
<img src="./static/1.gif" alt="" width="100%">
<p>
在长江南源,一处小头裸裂尻鱼新的栖息地被发现,鱼的数量大约超3万尾,为水生态保护提供了珍贵数据。
</p>
<p>
在长江中游,追踪显示,人工增殖放流的中华鲟成功入海率已经从45%左右提升至60%以上;鄱阳湖鱼类小型化、低龄化趋势得到遏制,栖息地生存环境得以改善。
</p>
<p>
在长江下游,今年3月起,南京秦淮河入江口首次出现野生中华绒螯蟹大规模洄游现象,种群数量明显增加。
</p>
<img src="./static/2.jpg" width="100%">
<p>
水生生物资源恢复向好,见证了长江十年禁渔三年多来的阶段性成果。
</p>
<p>
实施长江十年禁渔,是以同志为核心的党中央从中华民族长远利益出发作出的重要决策。党的十八大以来,总书记多次深入长江沿线考察调研,详细了解长江十年禁渔的实施情况,他指出,要坚定推进长江十年禁渔,巩固好已经取得的成果。
</p>
<img src="./static/3.jpg" width="100%">
<p>
按照部署,自2021年1月1日起,在长江干流、大型通江湖泊、重要支流和长江口部分海域实行为期十年的禁渔,常年禁止天然渔业资源的生产性捕捞。禁渔三年多来,相关评估显示,长江干流和鄱阳湖、洞庭湖水生生物完整性指数由禁渔前最差的“无鱼”提升了两个等级。2022年,长江江豚数量达到1249头,实现历史性止跌回升。长江干流水质连续4年全线保持Ⅱ类。
</p>
<p>
实施长江十年禁渔,解决好渔民上岸后的生产生活问题,禁渔才有稳定扎实的社会基础。
</p>
<img src="./static/4.jpg" width="100%">
<p>
安徽退捕转产的3万多名渔民,在政府的引导下接受就业培训。在当涂县,免费学习养殖技术,养殖生态螃蟹成了退捕渔民的新选择。
</p>
<p>
在拥有洞庭湖超六成水域的湖南岳阳,政府帮扶上岸渔民建起养殖场,发展风干鱼产业,还带领他们学习直播带货,拓宽销路。
</p>
<p>
在渔民退捕上岸的鄱阳湖棠荫岛,当地在继续保护好生态的前提下,正探索规划利用独特的自然资源发展旅游产业。禁渔三年多来,有关部门对23.1万退捕渔民逐一建档立卡,多渠道提升就业、社保水平。
</p>
<img src="./static/5.jpg" width="100%">
<p>
长江十年禁渔实施以来,沿江省市合力攻坚、久久为功,长江大保护不断向纵深推进,持续巩固禁渔成果。下一步,沿江省市还将加强水生生物重要栖息地修复,建立退捕渔民动态精准帮扶服务,完善跨区域、跨部门执法合作机制,确保一江清水绵延后世、惠泽人民。
</p>
<img src="./static/6.jpg" >
</div>
</body>
</html>

上述代码,最终经过浏览器的解析渲染之后,效果如下:

盒子模型

介绍
  • 盒子:页面中所有的元素(标签),都可以看做是一个 盒子,由盒子将页面中的元素包含在一个矩形区域内,通过盒子的视角更方便的进行页面布局。
  • 盒子模型组成:内容区域(content)、内边距区域(padding)、边框区域(border)、外边距区域(margin)。

CSS 盒子模型,其实和日常生活中的包装盒是非常类似的,就比如:

盒子的大小,其实就包括三个部分: border、padding、content,而 margin 外边距是不包括在盒子之内的。

布局标签
  • 布局标签:实际开发网页中,会大量频繁的使用 div 和 span 这两个没有语义的布局标签。

  • 标签:<div> <span>

  • 特点:

  • <div> 标签:

    • 一行只显示一个(独占一行)
    • 宽度默认是父元素的宽度,高度默认由内容撑开
    • 可以设置宽高(width、height)
  • <span> 标签:

    • 一行可以显示多个
    • 宽度和高度默认由内容撑开
    • 不可以设置宽高(width、height)
  • 测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<body>
<div>
A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A
</div>
<div>
A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A
</div>

<span>
A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A
</span>
<span>
A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A
</span>
</body>

浏览器打开后的效果:

1). div 会独占一行,默认宽度为父元素 body 的宽度。可以设置宽高(width、height)

2). span 一行会显示多个,用来组合行内元素,默认宽度为内容撑开的宽度。不可以设置宽高(width、height)

代码实现

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>盒子模型</title>
<style>
div {
width: 200px; /* 宽度 */
height: 200px; /* 高度 */
box-sizing: border-box; /* 指定width height为盒子的高宽 */
background-color: aquamarine; /* 背景色 */

padding: 20px 20px 20px 20px; /* 内边距, 上 右 下 左 , 边距都一行, 可以简写: padding: 20px;*/
border: 10px solid red; /* 边框, 宽度 线条类型 颜色 */
margin: 30px 30px 30px 30px; /* 外边距, 上 右 下 左 , 边距都一行, 可以简写: margin: 30px; */
}
</style>
</head>

<body>

<div>
A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A A
</div>

</body>
</html>

代码编写好了, 可以通过浏览器打开该页面, 通过开发者工具,我们就可以看到盒子的大小 , 以及盒子各个组成部分(内容、内边距、边框、外边距):

我们也可以,通过浏览器的开发者工具,清晰的看到这个盒子,以及每一个部分的大小:

备注:

  • 上述的 padding、margin 属性值,可以是 4 个值、也可以是两个值、也可以是一个值,具体的含义如下:
  • padding``: 20px 20px 20px 20px; -------> 表示上、右、下、左都是 20px;
  • padding: 20px 10px; ----------------------> 表示上下是 20px,左右是 10px;
  • padding: 20px; -----------------------------> 表示上、右、下、左都是 20px;

案例

需求

通过一个央视新闻页面的制作,大家已经熟悉了 HTML 中的常见标签及 CSS 中基础样式的写法及作用。 那接下来呢,我们就要通过一个案例,来加深大家对于这些标签和样式的掌握和使用,那么在完成这个案例的过程中呢,我们依然可以直接基于 AI 来辅助我们快速实现。

**需求:参照 Tlias 智能学习辅助系统,完成员工管理页面的制作。 **

产品经理制作的页面原型如下:

代码实现

顶部导航栏
基本实现

原型效果:

[!TIP]
提示词(prompt):

你是一名前端开发工程师,帮我生成一个 HTML 页面,页面整体有 4 个部分组成,先来实现第一个部分:

  1. 顶栏
    内容:包含左侧的标题“Tlias 智能学习辅助系统”,字体需加大加粗,以突出显示;右侧则放置“退出登录”文字链接。
    布局:确保标题与退出登录文本位于同一行内,分别左对齐与右对齐 。

页码代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tlias智能学习辅助系统</title>
<style>
body {
margin: 0;
}

/* 顶栏样式 */
.header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #f1f1f1;
padding: 10px 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

/* 加大加粗标题 */
.header h1 {
margin: 0;
font-size: 24px;
font-weight: bold;
}

/* 文本链接样式 */
.header a {
text-decoration: none;
color: #333;
font-size: 16px;
}
</style>
</head>
<body>
<!-- 顶栏 -->
<div class="header">
<h1>Tlias智能学习辅助系统</h1>
<a href="#">退出登录</a>
</div>

<!-- 其他部分可以在这里添加 -->
</body>
</html>

页面展示效果如下:

flex 布局

那在上述的案例代码中,其实我们用到了一种布局模式,叫 flex 布局

  • flex 是 flexible Box 的缩写,意为"弹性布局"。采用 flex 布局的元素,称为 Flex 容器(container),它的所有子元素自动成为容器成员,称为 Flex 项目(item)。
  • 通过给父容器添加 flex 属性,来控制子元素的位置和排列方式。

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#container {
display: flex;
/* justify-content: space-between; */ /* 先两边贴边,再平分剩余空间 */
/* justify-content: flex-start;*/ /* 从头开始排列 */
/* justify-content: flex-end; */ /* 从尾开始排列 */
/* justify-content: center; */ /* 居中排列 */
/* justify-content: space-around; */ /* 两边留白,中间平分,平分剩余空间 */
flex-direction: row;
justify-content: space-between;
background-color: #aeea6a;
width: 400px;
height: 300px;
}

#container div {
background-color: #e866ef;
width: 100px;
height: 50px;
}
</style>
</head>
<body>
<div id="container">
<div>Flex Item</div>
<div>Flex Item</div>
<div>Flex Item</div>
</div>
</body>
</html>
  • flex 布局相关的 CSS 样式:

如果主轴设置为 row,其实就是横向布局。 主轴设置为 column,其实就是纵向布局。

搜索表单

那接下来,我们要完成的是第二个部分,也就是搜索栏的制作。 页面原型展示如下:

那这里呢,需要用到 HTML 中的表单。 那接下来,我们先来介绍一下表单标签,然后再来实现搜索表单栏的制作。

表单标签

那表单呢,在我们日常的上网的过程中,基本上每天都会遇到。比如,我们经常在访问网站时,出现的登录页面、注册页面、个人信息提交页面,其实都是一个一个的表单 。 当我们在这些表单中录入数据之后,一点击 “提交”,就会将表单中我们填写的数据采集到,并提交, 那其实这个数据呢,一般会提交到服务端,最终保存在数据库中 (后面的课程中会讲到)。

那其实,上述的整个窗口是一个表单,而表单是一项一项的,这个我们称为表单项 或 表单元素。

  • 表单场景: 表单就是在网页中负责数据采集功能的,如:注册、登录的表单。

  • 表单标签: <form>

  • 表单属性:

    • action: 规定表单提交时,向何处发送表单数据,表单提交的 URL。
    • method: 规定用于发送表单数据的方式,常见为: GET、POST。
      • GET:表单数据是拼接在 url 后面的, 如: xxxxxxxxxxx?username=Tom&age=12,url 中能携带的表单数据大小是有限制的。
      • POST: 表单数据是在请求体(消息体)中携带的,大小没有限制。
  • 表单项标签: 不同类型的 input 元素、下拉列表、文本域等。

    • input: 定义表单项,通过 type 属性控制输入形式
    • select: 定义下拉列表
    • textarea: 定义文本域

演示:

1). GET 方式提交的表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTML-表单</title>
</head>
<body>
<!--
form表单属性:
action: 表单提交的url, 往何处提交数据 . 如果不指定, 默认提交到当前页面
method: 表单的提交方式 .
get: 在url后面拼接表单数据, 比如: ?username=Tom&age=12 , url长度有限制 . 默认值
post: 在消息体(请求体)中传递的, 参数大小无限制的.
-->

<form action="" method="get">
用户名: <input type="text" name="username">
年龄: <input type="text" name="age">

<input type="submit" value="提交">
</form>

</body>
</html>

表单编写完毕之后,通过浏览器打开此表单,然后再表单项中录入值之后,点击提交,我们会看到表单的数据在 url 后面提交到服务端,格式为:?username=Tom&age=12。

2). POST 方式提交表单

将上述的表单提交方式由 get,改为 post

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTML-表单</title>
</head>
<body>
<!--
form表单属性:
action: 表单提交的url, 往何处提交数据 . 如果不指定, 默认提交到当前页面
method: 表单的提交方式 .
get: 在url后面拼接表单数据, 比如: ?username=Tom&age=12 , url长度有限制 . 默认值
post: 在消息体(请求体)中传递的, 参数大小无限制的.
-->

<form action="" method="post">
用户名: <input type="text" name="username">
年龄: <input type="text" name="age">

<input type="submit" value="提交">
</form>

</body>
</html>

表单编写完毕之后,通过浏览器打开此表单,然后再表单项中录入值之后,点击提交,我们会看到表单的数据在 url 后面提交到服务端,格式为:?username=Tom&age=12。

注意事项:

表单中的所有表单项,要想能够正常的采集数据,在提交的时候能提交到服务端,表单项必须指定 name 属性。 否则,无法提交该表单项。

1
用户名: <input type="text" name="username">
表单项标签

在一个表单中,可以存在很多的表单项,而虽然表单项的形式各式各样,但是表单项的标签其实就只有三个,分别是:

  • <input>: 表单项 , 通过 type 属性控制输入形式。
  • <select>: 定义下拉列表, <option> 定义列表项
  • <textarea>: 文本域

演示:

创建一个新的表单项的 html 文件,具体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTML-表单项标签</title>
</head>
<body>

<!-- value: 表单项提交的值 -->
<form action="/save" method="post">
姓名: <input type="text" name="name"> <br><br>

密码: <input type="password" name="password"> <br><br>

性别: <input type="radio" name="gender" value="1">
<label><input type="radio" name="gender" value="2"></label> <br><br>

爱好: <label><input type="checkbox" name="hobby" value="java"> java </label>
<label><input type="checkbox" name="hobby" value="game"> game </label>
<label><input type="checkbox" name="hobby" value="sing"> sing </label> <br><br>

图像: <input type="file" name="image"> <br><br>

生日: <input type="date" name="birthday"> <br><br>

时间: <input type="time" name="time"> <br><br>

日期时间: <input type="datetime-local" name="datetime"> <br><br>

学历: <select name="degree">
<option value="">----------- 请选择 -----------</option>
<option value="1">大专</option>
<option value="2">本科</option>
<option value="3">硕士</option>
<option value="4">博士</option>
</select> <br><br>

描述: <textarea name="description" cols="30" rows="10"></textarea> <br><br>

<input type="hidden" name="id" value="1">

<!-- 表单常见按钮 -->
<input type="button" value="按钮">
<input type="reset" value="重置">
<input type="submit" value="提交">
<br>
</form>

</body>
</html>

通过浏览器打开上述的表单项 html 文件,最终展示出的表单信息如下:

而对于 <input type="hidden">,是一个隐藏域,在表单中并不会显示出来,但是在提交表单的时候,是会提交到服务端的。 接下来,我们就点击提交按钮,来提交当前表单,看看提交的数据:

搜索表单实现

那基本的表单标签和表单项标签,讲解完毕后,接下来呢,我们就来完成搜索表单的实现,同样,我们可以借助于 AI 帮我们完成对应的页面布局。

[!TIP]
提示词(prompt):

再继续帮我生成第二个部分: 2. 搜索表单区域
组成:包括三个输入控件和两个操作按钮。输入控件具体为:姓名(文本输入框)、性别(下拉选择,选项包括 男/女, 默认为空)、职位(下拉选择,选项包括班主任、讲师、学工主管、教研主管、咨询师, 默认为空)。
按钮:“查询”与“清空”按钮,用于提交表单或重置表单项 。
布局:所有表单项及按钮需水平排列于一行 ,确保美观大气 。

代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tlias智能学习辅助系统</title>
<style>
body {
margin: 0;
}

/* 顶栏样式 */
.header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #f1f1f1;
padding: 10px 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

/* 加大加粗标题 */
.header h1 {
margin: 0;
font-size: 24px;
font-weight: bold;
}

/* 文本链接样式 */
.header a {
text-decoration: none;
color: #333;
font-size: 16px;
}

/* 搜索表单区域 */
.search-form {
display: flex;
align-items: center;
padding: 20px;
background-color: #f9f9f9;
}

/* 表单控件样式 */
.search-form input[type="text"], .search-form select {
margin-right: 10px;
padding: 5px 10px;
border: 1px solid #ccc;
border-radius: 4px;
width: 200px;
}

/* 按钮样式 */
.search-form button {
padding: 5px 15px;
margin-left: 10px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}

/* 清空按钮样式 */
.search-form button.clear {
background-color: #6c757d;
}
</style>
</head>
<body>
<!-- 顶栏 -->
<div class="header">
<h1>Tlias智能学习辅助系统</h1>
<a href="#">退出登录</a>
</div>

<!-- 搜索表单区域 -->
<form class="search-form">
<input type="text" name="name" placeholder="姓名" />
<select name="gender">
<option value="">性别</option>
<option value="1"></option>
<option value="2"></option>
</select>
<select name="job">
<option value="">职位</option>
<option value="1">班主任</option>
<option value="2">讲师</option>
<option value="3">学工主管</option>
<option value="4">教研主管</option>
<option value="5">咨询师</option>
</select>
<button type="submit">查询</button>
<button type="reset" class="clear">清空</button>
</form>

</body>
</html>

浏览器呈现效果为:

表格数据展示
基本实现

页面效果如下:

[!TIP]
提示词(prompt):

再继续帮我生成第三个部分: 3. 表格展示区
表格结构:展示列包括姓名、性别(显示男/女)、头像(小图标展示)、职位(显示班主任/讲师/学工主管/教研主管/咨询师)、入职日期、最后操作时间、操作(里包含两个按钮 编辑 与 删除)。
数据模拟:基于《笑傲江湖》小说人物生成 4 条虚拟数据,每条数据应包含上述所有列的信息,以体现实际应用场景 。
样式:可适当调整表格样式,确保美观大气。

代码实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tlias智能学习辅助系统</title>
<style>
body {
margin: 0;
}

/* 顶栏样式 */
.header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #f1f1f1;
padding: 10px 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

/* 加大加粗标题 */
.header h1 {
margin: 0;
font-size: 24px;
font-weight: bold;
}

/* 文本链接样式 */
.header a {
text-decoration: none;
color: #333;
font-size: 16px;
}

/* 搜索表单区域 */
.search-form {
display: flex;
align-items: center;
padding: 20px;
background-color: #f9f9f9;
}

/* 表单控件样式 */
.search-form input[type="text"], .search-form select {
margin-right: 10px;
padding: 5px 10px;
border: 1px solid #ccc;
border-radius: 4px;
width: 200px;
}

/* 按钮样式 */
.search-form button {
padding: 5px 15px;
margin-left: 10px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}

/* 清空按钮样式 */
.search-form button.clear {
background-color: #6c757d;
}

.table {
min-width: 100%;
border-collapse: collapse;
margin: 0 20px;
}

/* 设置表格单元格边框 */
.table td, .table th {
border: 1px solid #ddd;
padding: 8px;
text-align: center;
}

.avatar {
width: 50px;
height: 50px;
object-fit: cover;
border-radius: 50%;
}

</style>
</head>
<body>
<!-- 顶栏 -->
<div class="header">
<h1>Tlias智能学习辅助系统</h1>
<a href="#">退出登录</a>
</div>

<!-- 搜索表单区域 -->
<form class="search-form" action="#" method="post">
<input type="text" name="name" placeholder="姓名" />
<select name="gender">
<option value="">性别</option>
<option value="1"></option>
<option value="2"></option>
</select>
<select name="job">
<option value="">职位</option>
<option value="1">班主任</option>
<option value="2">讲师</option>
<option value="3">学工主管</option>
<option value="4">教研主管</option>
<option value="5">咨询师</option>
</select>
<button type="submit">查询</button>
<button type="reset" class="clear">清空</button>
</form>

<table class="table table-striped table-bordered">
<thead>
<tr>
<th>姓名</th>
<th>性别</th>
<th>头像</th>
<th>职位</th>
<th>入职日期</th>
<th>最后操作时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>令狐冲</td>
<td></td>
<td><img src="https://via.placeholder.com/50" alt="令狐冲" class="avatar"></td>
<td>讲师</td>
<td>2021-03-15</td>
<td>2023-07-30T12:00:00Z</td>
<td class="btn-group">
<button>编辑</button>
<button>删除</button>
</td>
</tr>
<tr>
<td>任盈盈</td>
<td></td>
<td><img src="https://via.placeholder.com/50" alt="任盈盈" class="avatar"></td>
<td>学工主管</td>
<td>2020-04-10</td>
<td>2023-07-29T15:00:00Z</td>
<td class="btn-group">
<button>编辑</button>
<button>删除</button>
</td>
</tr>
<tr>
<td>岳不群</td>
<td></td>
<td><img src="https://via.placeholder.com/50" alt="岳不群" class="avatar"></td>
<td>教研主管</td>
<td>2019-01-01</td>
<td>2023-07-30T10:00:00Z</td>
<td class="btn-group">
<button>编辑</button>
<button>删除</button>
</td>
</tr>
<tr>
<td>宁中则</td>
<td></td>
<td><img src="https://via.placeholder.com/50" alt="宁中则" class="avatar"></td>
<td>班主任</td>
<td>2018-06-01</td>
<td>2023-07-29T09:00:00Z</td>
<td class="btn-group">
<button>编辑</button>
<button>删除</button>
</td>
</tr>
</tbody>
</table>

</body>
</html>

页面展示的效果为:

表格标签
底部版权区域

页面原型展示如下:

[!TIP]
提示词(prompt):

再继续帮我生成第二个部分: 4. 页脚版权区域
内容:第一行显示公司全称“江苏传智播客教育科技股份有限公司”;第二行展示版权信息,“版权所有 Copyright 2006-2024 All Rights Reserved”。
设计:该区域应具有灰色背景,字体颜色为白色,居中对齐,以营造专业且统一的视觉效果 。

页面代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tlias智能学习辅助系统</title>
<style>
body {
margin: 0;
}

/* 顶栏样式 */
.header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #f1f1f1;
padding: 10px 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

/* 加大加粗标题 */
.header h1 {
margin: 0;
font-size: 24px;
font-weight: bold;
}

/* 文本链接样式 */
.header a {
text-decoration: none;
color: #333;
font-size: 16px;
}

/* 搜索表单区域 */
.search-form {
display: flex;
align-items: center;
padding: 20px;
background-color: #f9f9f9;
}

/* 表单控件样式 */
.search-form input[type="text"], .search-form select {
margin-right: 10px;
padding: 5px 10px;
border: 1px solid #ccc;
border-radius: 4px;
width: 200px;
}

/* 按钮样式 */
.search-form button {
padding: 5px 15px;
margin-left: 10px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}

/* 清空按钮样式 */
.search-form button.clear {
background-color: #6c757d;
}

.table {
min-width: 100%;
border-collapse: collapse;
margin: 0 20px;
}

/* 设置表格单元格边框 */
.table td, .table th {
border: 1px solid #ddd;
padding: 8px;
text-align: center;
}

.avatar {
width: 50px;
height: 50px;
object-fit: cover;
border-radius: 50%;
}

/* 页脚版权区域 */
.footer {
background-color: #8f8c8c;
color: white;
text-align: center;
padding: 20px 0;
margin-top: 30px;
}

.footer .company-name {
font-size: 1.1em;
font-weight: bold;
}

.footer .copyright {
font-size: 0.9em;
}
</style>
</head>
<body>
<!-- 顶栏 -->
<div class="header">
<h1>Tlias智能学习辅助系统</h1>
<a href="#">退出登录</a>
</div>

<!-- 搜索表单区域 -->
<form class="search-form" action="#" method="post">
<input type="text" name="name" placeholder="姓名" />
<select name="gender">
<option value="">性别</option>
<option value="1"></option>
<option value="2"></option>
</select>
<select name="job">
<option value="">职位</option>
<option value="1">班主任</option>
<option value="2">讲师</option>
<option value="3">学工主管</option>
<option value="4">教研主管</option>
<option value="5">咨询师</option>
</select>
<button type="submit">查询</button>
<button type="reset" class="clear">清空</button>
</form>

<table class="table table-striped table-bordered">
<thead>
<tr>
<th>姓名</th>
<th>性别</th>
<th>头像</th>
<th>职位</th>
<th>入职日期</th>
<th>最后操作时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>令狐冲</td>
<td></td>
<td><img src="https://via.placeholder.com/50" alt="令狐冲" class="avatar"></td>
<td>讲师</td>
<td>2021-03-15</td>
<td>2023-07-30T12:00:00Z</td>
<td class="btn-group">
<button>编辑</button>
<button>删除</button>
</td>
</tr>
<tr>
<td>任盈盈</td>
<td></td>
<td><img src="https://via.placeholder.com/50" alt="任盈盈" class="avatar"></td>
<td>学工主管</td>
<td>2020-04-10</td>
<td>2023-07-29T15:00:00Z</td>
<td class="btn-group">
<button>编辑</button>
<button>删除</button>
</td>
</tr>
<tr>
<td>岳不群</td>
<td></td>
<td><img src="https://via.placeholder.com/50" alt="岳不群" class="avatar"></td>
<td>教研主管</td>
<td>2019-01-01</td>
<td>2023-07-30T10:00:00Z</td>
<td class="btn-group">
<button>编辑</button>
<button>删除</button>
</td>
</tr>
<tr>
<td>宁中则</td>
<td></td>
<td><img src="https://via.placeholder.com/50" alt="宁中则" class="avatar"></td>
<td>班主任</td>
<td>2018-06-01</td>
<td>2023-07-29T09:00:00Z</td>
<td class="btn-group">
<button>编辑</button>
<button>删除</button>
</td>
</tr>
</tbody>
</table>

<!-- 页脚版权区域 -->
<footer class="footer">
<p class="company-name">江苏传智播客教育科技股份有限公司</p>
<p class="copyright">版权所有 Copyright 2006-2024 All Rights Reserved</p>
</footer>

</body>
</html>

页面展示如下:

版心居中

这个案例类似于央视新闻页面,页面中的内容,都需要居中显示,所以这里呢,我们就可以使用盒子模型来进行布局。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tlias智能学习辅助系统</title>
<style>
body {
margin: 0;
}

/* 顶栏样式 */
.header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #c2c0c0;
padding: 10px 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

/* 加大加粗标题 */
.header h1 {
margin: 0;
font-size: 24px;
font-weight: bold;
}

/* 文本链接样式 */
.header a {
text-decoration: none;
color: #333;
font-size: 16px;
}

/* 搜索表单区域 */
.search-form {
display: flex;
align-items: center;
padding: 20px;
background-color: #f9f9f9;
}

/* 表单控件样式 */
.search-form input[type="text"], .search-form select {
margin-right: 10px;
padding: 5px 10px;
border: 1px solid #ccc;
border-radius: 4px;
width: 200px;
}

/* 按钮样式 */
.search-form button {
padding: 5px 15px;
margin-left: 10px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}

/* 清空按钮样式 */
.search-form button.clear {
background-color: #6c757d;
}

.table {
min-width: 100%;
border-collapse: collapse;
}

/* 设置表格单元格边框 */
.table td, .table th {
border: 1px solid #ddd;
padding: 8px;
text-align: center;
}

.avatar {
width: 30px;
height: 30px;
object-fit: cover;
border-radius: 50%;
}

/* 页脚版权区域 */
.footer {
background-color: #c2c0c0;
color: white;
text-align: center;
padding: 10px 0;
margin-top: 30px;
}

.footer .company-name {
font-size: 1.1em;
font-weight: bold;
}

.footer .copyright {
font-size: 0.9em;
}

#container {
width: 80%;
margin: 0 auto;
}
</style>
</head>
<body>

<div id="container">
<!-- 顶栏 -->
<div class="header">
<h1>Tlias智能学习辅助系统</h1>
<a href="#">退出登录</a>
</div>

<!-- 搜索表单区域 -->
<form class="search-form" action="#" method="post">
<input type="text" name="name" placeholder="姓名" />
<select name="gender">
<option value="">性别</option>
<option value="male"></option>
<option value="female"></option>
</select>
<select name="position">
<option value="">职位</option>
<option value="班主任">班主任</option>
<option value="讲师">讲师</option>
<option value="学工主管">学工主管</option>
<option value="教研主管">教研主管</option>
<option value="咨询师">咨询师</option>
</select>
<button type="submit">查询</button>
<button type="reset" class="clear">清空</button>
</form>

<table class="table table-striped table-bordered">
<thead>
<tr>
<th>姓名</th>
<th>性别</th>
<th>头像</th>
<th>职位</th>
<th>入职日期</th>
<th>最后操作时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>令狐冲</td>
<td></td>
<td><img src="https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg" alt="令狐冲" class="avatar"></td>
<td>讲师</td>
<td>2021-03-15</td>
<td>2023-07-30T12:00:00Z</td>
<td class="btn-group">
<button>编辑</button>
<button>删除</button>
</td>
</tr>
<tr>
<td>任盈盈</td>
<td></td>
<td><img src="https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg" alt="任盈盈" class="avatar"></td>
<td>学工主管</td>
<td>2020-04-10</td>
<td>2023-07-29T15:00:00Z</td>
<td class="btn-group">
<button>编辑</button>
<button>删除</button>
</td>
</tr>
<tr>
<td>岳不群</td>
<td></td>
<td><img src="https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg" alt="岳不群" class="avatar"></td>
<td>教研主管</td>
<td>2019-01-01</td>
<td>2023-07-30T10:00:00Z</td>
<td class="btn-group">
<button>编辑</button>
<button>删除</button>
</td>
</tr>
<tr>
<td>宁中则</td>
<td></td>
<td><img src="https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg" alt="宁中则" class="avatar"></td>
<td>班主任</td>
<td>2018-06-01</td>
<td>2023-07-29T09:00:00Z</td>
<td class="btn-group">
<button>编辑</button>
<button>删除</button>
</td>
</tr>
</tbody>
</table>

<!-- 页脚版权区域 -->
<footer class="footer">
<p class="company-name">江苏传智播客教育科技股份有限公司</p>
<p class="copyright">版权所有 Copyright 2006-2024 All Rights Reserved</p>
</footer>

</div>

</body>
</html>

页面效果如下:

02-前端 Web 开发(JS+Vue+Ajax)

介绍

在前面的课程中,我们已经学习了 HTML、CSS 的基础内容,我们知道 HTML 负责网页的结构,而 CSS 负责的是网页的表现。 而要想让网页具备一定的交互效果,具有一定的动作行为,还得通过 JavaScript 来实现。那今天,我们就来讲解 JavaScript,这门语言会让我们的页面能够和用户进行交互。

那什么是 JavaScript 呢 ?

JavaScript(简称:JS) 是一门跨平台、面向对象的脚本语言,是用来控制网页行为的,实现人机交互效果。JavaScript 和 Java 是完全不同的语言,不论是概念还是设计。但是基础语法类似。

  • 组成:
    • ECMAScript: 规定了 JS 基础语法核心知识,包括变量、数据类型、流程控制、函数、对象等。
    • BOM:浏览器对象模型,用于操作浏览器本身,如:页面弹窗、地址栏操作、关闭窗口等。
    • DOM:文档对象模型,用于操作 HTML 文档,如:改变标签内的内容、改变标签内字体样式等。

[!TIP]
备注:ECMA 国际(前身为欧洲计算机制造商协会),制定了标准化的脚本程序设计语言 ECMAScript,这种语言得到广泛应用。而 JavaScript 是遵守 ECMAScript 的标准的(ES2024 是最新版本)。

JS 核心语法

JS 引入方式

同样,js 代码也是书写在 html 中的,那么 html 中如何引入 js 代码呢?主要通过下面的 2 种引入方式:

  • 第一种方式:内部脚本,将 JS 代码定义在 HTML 页面中

    • JavaScript 代码必须位于标签之间
    • 在 HTML 文档中,可以在任意地方,放置任意数量的
    • 一般会把脚本置于元素的底部,可改善显示速度
    • 例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
      ```

    <!DOCTYPE html>

    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JS 引入方式</title>
    </head>
    <body>

    <script>
    alert('Hello JS')
    </script>

    </body>
    </html>
  • 第二种方式:外部脚本, 将 JS 代码定义在外部 JS 文件中,然后引入到 HTML 页面中

    • 外部 JS 文件中,只包含 JS 代码,不包含

      1
      2

      </body>

      打开浏览器运行之后,大家会发现,可以正常执行,第一次弹出 B,第二次弹出 C 。我们看到 name变量重复声明了,但是呢,如果使用var关键字,是没有问题的,可以重复声明。
      var****声明的变量呢,还有一些其他不严谨的地方,这里就不再一一列举了,所以这个声明变量的关键字,并不严谨 【不推荐】。

      2.2.2 常量

      在 JS 中,如果声明一个场景,需要使用 const 关键字。一旦声明,常量的值就不能改变 (不可以重新赋值)。

      如下所示:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      <body>

      <script>
      //常量
      const PI = 3.14;
      PI = 3.15;
      alert(PI);
      </script>
      </body>

      浏览器打开之后,会报如下错误:

      该错误就表示,常量不可以被重新分配值。

      数据类型

      虽然 JS 是弱数据类型的语言,但是 JS 中也存在数据类型,JS 中的数据类型分为 :原始数据类型 和 引用数据类型。那这部分,我们先来学习原始数据类型,主要包含以下几种类型:

      使用 typeof 关键字可以返回变量的数据类型,接下来我们需要通过书写代码来演示 js 中的数据类型。代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>JS-数据类型</title>
      </head>
      <body>

      <script>
      //原始数据类型
      alert(typeof 3); //number
      alert(typeof 3.14); //number

      alert(typeof "A"); //string
      alert(typeof 'Hello');//string

      alert(typeof true); //boolean
      alert(typeof false);//boolean

      alert(typeof null); //object

      var a ;
      alert(typeof a); //undefined

      </script>
      </body>
      </html>

      对于字符串类型的数据,除了可以使用双引号(“…”)、单引号(‘…’)以外,还可以使用反引号 (``)。 而使用反引号引起来的字符串,也称为 模板字符串

      • 模板字符串的使用场景:拼接字符串和变量。

      • 模板字符串的语法:

        • ... :反引号 (英文输入模式下键盘 tab 键上方波浪线 ~ 那个键)
        • 内容拼接时,使用 ${ } 来引用变量

      具体示例如下:

      1
      2
      3
      4
      5
      6
      <script>
          let name = 'Tom';
          let age = 18;
      console.log('大家好, 我是新入职的' + name + ', 今年' + age + '岁了, 请多多关照'); //原始方式 , 手动拼接字符串
          console.log(`大家好, 我是新入职的${name}, 今年${age}岁了, 请多多关照`); //使用模板字符串方式拼接字符串
        </script>

      函数

      **函数(function)**是被设计用来执行特定任务的代码块,方便程序的封装复用。 那我们学习函数,主要就是学习 JS 中函数的定义及调用的语法。

      方式一

      格式如下:

      1
      2
      3
      function 函数名(参数1,参数2..){
      要执行的代码
      }

      因为 JavaScript 是弱数据类型的语言,所以有如下几点需要注意:

      • 形参不需要声明类型,并且 JS 中不管什么类型都是 let 去声明,加上也没有意义。
      • 返回值也不需要声明类型,直接 return 即可

      示例:

      1
      2
      3
      function add(a, b){
      return a + b;
      }

      如果要调用上述的函数 add,可以使用:函数名称(实际参数列表)

      1
      2
      let result = add(10,20);
      alert(result);

      我们在调用 add 函数时,再添加 2 个参数,修改代码如下:

      1
      2
      var result = add(10,20,30,40);
      alert(result);

      浏览器打开,发现没有错误,并且依然弹出 30,这是为什么呢?

      因为在 JavaScript 中,函数的调用只需要名称正确即可,参数列表不管的。如上述案例,10 传递给了变量 a,20 传递给了变量 b,而 30 和 40 没有变量接受,但是不影响函数的正常调用。

      **注意:由于 JS 是弱类型语言,形参、返回值都不需要指定类型。在调用函数时,实参个数与形参个数可以不一致,但是建议一致。 **

      方式二

      刚才我们定义函数,是为函数指定了一个名字。 那我们也可以不为函数指定名字,那这一类的函数,我们称之为匿名函数。那接下来,方式二,就来介绍一下匿名函数的定义和调用。

      **匿名函数:**是指一种没有名称的函数,由于它们没有名称,因此无法直接通过函数名来调用,而是通过变量或表达式来调用。

      匿名函数定义可以通过两种方式:函数表达式 和 箭头函数。

      • 示例一(函数表达式):
      1
      2
      3
      var add = function (a,b){
      return a + b;
      }
      • 示例二(箭头函数):
      1
      2
      3
      var add = (a,b) => {
      return a + b;
      }

      上述匿名函数声明好了之后,是将这个函数赋值给了 add 变量。 那我们就可以直接通过 add 函数直接调用,调用代码如下:

      1
      2
      let result = add(10,20);
      alert(result);

      而箭头函数这种形式,在现在的前端开发中用的会更多一些。

      自定义对象

      在 JavaScript 中自定义对象特别简单,其语法格式如下:

      1
      2
      3
      4
      5
      6
      let 对象名 = {
      属性名1: 属性值1,
      属性名2: 属性值2,
      属性名3: 属性值3,
      方法名称: function(形参列表){}
      };

      我们可以通过如下语法调用属性:

      1
      对象名.属性名

      通过如下语法调用函数:

      1
      对象名.方法名()

      代码演示:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <script>
      //自定义对象
      let user = {
      name: "Tom",
      age: 10,
      gender: "男",
      sing: function(){
      console.log("悠悠的唱着最炫的民族风~");
      }
      }

      console.log(user.name);
      user.eat();
      <script>

      浏览器控制台结果如下:

      其中,上述函数定义的语法可以简化成如下格式:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <script>
      //自定义对象
      let user = {
      name: "Tom",
      age: 10,
      gender: "男",
      sing(){
      console.log("悠悠的唱着最炫的民族风~");
      }
      }

      console.log(user.name);
      user.eat();
      <script>
      JSON

      JSON 对象:JavaScript Object Notation,JavaScript 对象标记法。JSON 是通过 JavaScript 标记法书写的文本。其格式如下:

      1
      2
      3
      4
      5
      {
      "key":value,
      "key":value,
      "key":value
      }

      其中,key 必须使用引号并且是双引号标记,value 可以是任意数据类型。

      而由于语法简单,层级结构鲜明,现多用于作为数据载体,在网络中进行数据传输。

      代码演示:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      //3. JSON - JS对象标记法
      let person = {
      name: 'itcast',
      age: 18,
      gender: '男'
      }
      alert(JSON.stringify(person)); //js对象 --> json字符串

      let personJson = '{"name": "heima", "age": 18}';
      alert(JSON.parse(personJson).name);

      API 说明:

      [!TIP]
      JSON.stringify(…):作用就是将 js 对象,转换为 json 格式的字符串。
      JSON.parse(…):作用就是将 json 格式的字符串,转为 js 对象。

      流程控制

      在 JS 中,当然也存在对应的流程控制语句。常见的流程控制语句如下:

      • if … else if … else …
      • switch
      • for
      • while
      • do … while

      而 JS 中的流程控制语句与 JAVA 中的流程控制语句的作用,执行机制都是一样的。这个呢,我们现在就不再一个一个的如研究了,后面用到的时候,我们再做说明。

      JS DOM

      DOM 介绍

      DOM:Document Object Model 文档对象模型。也就是 JavaScript 将 HTML 文档的各个组成部分封装为对象。

      DOM 其实我们并不陌生,之前在学习 XML 就接触过,只不过 XML 文档中的标签需要我们写代码解析,而 HTML 文档是浏览器解析。封装的对象分为

      • Document:整个文档对象
      • Element:元素对象
      • Attribute:属性对象
      • Text:文本对象
      • Comment:注释对象

      如下图,左边是 HTML 文档内容,右边是 DOM 树

      那么我们学习 DOM 技术有什么用呢?主要作用如下:

      • 改变 HTML 元素的内容
      • 改变 HTML 元素的样式(CSS)
      • 对 HTML DOM 事件作出反应
      • 添加和删除 HTML 元素
      DOM 操作
      • DOM 的核心思想:将网页的内容当做对象来处理,标签的所有属性在该对象上都可以找到,并且修改这个对象的属性,就会自动映射到标签身上。

      • document 对象

        • 网页中所有内容都封装在 document 对象中
        • 它提供的属性和方法都是用来访问和操作网页内容的,如:document.write(…)
      • DOM 操作步骤:

        • 获取 DOM 元素对象
        • 操作 DOM 对象的属性或方法 (查阅文档)
      • 我们可以通过如下两种方式来获取 DOM 元素。

        1. 根据 CSS 选择器来获取 DOM 元素,获取到匹配到的第一个元素:document.querySelector(``'``CSS选择器``'``);

        2. 根据 CSS 选择器来获取 DOM 元素,获取匹配到的所有元素:document.querySelectorAll('CSS选择器');

          注意:获取到的所有元素,会封装到一个 NodeList 节点集合中,是一个伪数组(有长度、有索引的数组,但没有 push、pop 等数组方法)

      代码演示:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>JS-DOM</title>
      </head>
      <body>

      <h1 id="title1">11111</h1>
      <h1>22222</h1>
      <h1>33333</h1>

      <script>
      //1. 修改第一个h1标签中的文本内容
      //1.1 获取DOM对象
      // let h1 = document.querySelector('#title1');
      //let h1 = document.querySelector('h1'); // 获取第一个h1标签

      let hs = document.querySelectorAll('h1');

      //1.2 调用DOM对象中属性或方法
      hs[0].innerHTML = '修改后的文本内容';
      </script>
      </body>
      </html>

      PS:在早期的 JS 中,我们也可以通过如下方法获取 DOM 元素(了解)。

      • document.getElementById(…):根据 id 属性值获取,返回单个 Element 对象。
      • document.getElementsByTagName(…):根据标签名称获取,返回 Element 对象数组。
      • document.getElementsByName():根据 name 属性值获取,返回 Element 对象数组。
      • document.getElementsByClassName():根据 class 属性值获取,返回 Element 对象数组。

      JS 事件监听

      事件介绍

      什么是事件呢?HTML 事件是发生在 HTML 元素上的 “事情”,例如:

      • 按钮被点击
      • 鼠标移到元素上
      • 输入框失去焦点
      • 按下键盘按键

      而我们可以给这些事件绑定函数,当事件触发时,可以自动的完成对应的功能,这就是事件监听。

      例如:对于我们所说的百度注册页面,我们给用户名输入框的失去焦点事件绑定函数,当我们用户输入完内容,在标签外点击了鼠标,对于用户名输入框来说,失去焦点,然后执行绑定的函数,函数进行用户名内容的校验等操作。

      JS 事件是 JS 非常重要的一部分,接下来我们进行事件的学习。那么我们对于 JavaScript 事件需要学习哪些内容呢?我们得知道有哪些常用事件,然后我们得学会如何给事件绑定函数。

      所以主要围绕 2 点来学习:①. 事件监听、②. 常用事件

      事件监听语法

      JS 事件监听的语法:

      1
      事件源.addEventListener('事件类型', 要执行的函数);

      在上述的语法中包含三个要素:

      • 事件源: 哪个 dom 元素触发了事件, 要获取 dom 元素
      • 事件类型: 用什么方式触发, 比如: 鼠标单击 click, 鼠标经过 mouseover
      • 要执行的函数: 要做什么事

      演示:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>JS-事件-事件绑定</title>
      </head>

      <body>
      <input type="button" id="btn1" value="点我一下试试1">
      <input type="button" id="btn2" value="点我一下试试2">

      <script>
      document.querySelector("#btn1").addEventListener('click', ()=>{
      alert("按钮1被点击了...");
      })
      </script>
      </body>
      </html>

      JavaScript 对于事件的绑定还提供了另外 2 种方式(早期版本):

      1). 通过 html 标签中的事件属性进行绑定

      例如一个按钮,我们对于按钮可以绑定单机事件,可以借助标签的 onclick 属性,属性值指向一个函数。

      1
      2
      3
      4
      5
      6
      7
      <input type="button" id="btn1" value="点我一下试试1" onclick="on()">

      <script>
      function on(){
      alert('试试就试试')
      }
      </script>

      2). 通过 DOM 中 Element 元素的事件属性进行绑定

      依据我们学习过得 DOM 的知识点,我们知道 html 中的标签被加载成 element 对象,所以我们也可以通过 element 对象的属性来操作标签的属性。

      例如一个按钮,我们对于按钮可以绑定单机事件,可以通过 DOM 元素的属性,为其做事件绑定。

      1
      2
      3
      4
      5
      6
      7
      8
      <body>
      <input type="button" id="btn1" value="点我一下试试1">
      <script>
      document.querySelector('#btn1').onclick = function(){
      alert("按钮2被点击了...");
      }
      </script>
      </body>

      整体代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>JS-事件-事件绑定</title>
      </head>

      <body>
      <input type="button" id="btn1" value="事件绑定1">
      <input type="button" id="btn2" value="事件绑定2">
      <script>
      document.querySelector("#btn1").addEventListener('click', ()=>{
      alert("按钮1被点击了...");
      })

      document.querySelector('#btn2').onclick = function(){
      alert("按钮2被点击了...");
      }
      </script>
      </body>
      </html>

      addEventListener 与 on 事件 区别:

      • on 方式会被覆盖,addEventListener 方式可以绑定多次,拥有更多特性,推荐使用 addEventListener .

      隔行换色案例

      **需求:**实现鼠标移入数据行时,背景色改为#f2e2e2,鼠标移出时,再将背景色改为白色。

      效果:

      这个功能,我们就可以直接基于通义零码来帮我们实现了。

      [!TIP]
      提示词(prompt):
      通过 js 实现鼠标移入移出效果,鼠标移入,背景色为 #f2e2e2,鼠标移出,背景色为 白色。

      最终完整代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
      282
      283
      284
      285
      286
      287
      288
      289
      290
      291
      292
      293
      294
      295
      296
      297
      298
      299
      300
      301
      302
      303
      304
      305
      306
      307
      308
      309
      310
      311
      312
      313
      314
      315
      316
      317
      318
      319
      320
      321
      322
      323
      324
      325
      326
      327
      328
      329
      330
      331
      332
      333
      334
      <!DOCTYPE html>
      <html lang="zh-CN">
      <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Tlias智能学习辅助系统</title>
      <style>
      body {
      margin: 0;
      }

      /* 顶栏样式 */
      .header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      background-color: #c2c0c0;
      padding: 20px 20px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
      }

      /* 加大加粗标题 */
      .header h1 {
      margin: 0;
      font-size: 24px;
      font-weight: bold;
      }

      /* 文本链接样式 */
      .header a {
      text-decoration: none;
      color: #333;
      font-size: 16px;
      }

      /* 搜索表单区域 */
      .search-form {
      display: flex;
      align-items: center;
      padding: 20px;
      background-color: #f9f9f9;
      }

      /* 表单控件样式 */
      .search-form input[type="text"], .search-form select {
      margin-right: 10px;
      padding: 10px 10px;
      border: 1px solid #ccc;
      border-radius: 4px;
      width: 26%;
      }

      /* 按钮样式 */
      .search-form button {
      padding: 10px 15px;
      margin-left: 10px;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      }

      /* 清空按钮样式 */
      .search-form button.clear {
      background-color: #6c757d;
      }

      .table {
      min-width: 100%;
      border-collapse: collapse;
      }

      /* 设置表格单元格边框 */
      .table td, .table th {
      border: 1px solid #ddd;
      padding: 8px;
      text-align: center;
      }

      .avatar {
      width: 30px;
      height: 30px;
      object-fit: cover;
      border-radius: 50%;
      }

      /* 页脚版权区域 */
      .footer {
      background-color: #c2c0c0;
      color: white;
      text-align: center;
      padding: 10px 0;
      margin-top: 30px;
      }

      .footer .company-name {
      font-size: 1.1em;
      font-weight: bold;
      }

      .footer .copyright {
      font-size: 0.9em;
      }

      #container {
      width: 80%;
      margin: 0 auto;
      }
      </style>
      </head>
      <body>

      <div id="container">
      <!-- 顶栏 -->
      <div class="header">
      <h1>Tlias智能学习辅助系统</h1>
      <a href="#">退出登录</a>
      </div>

      <!-- 搜索表单区域 -->
      <form class="search-form" action="#" method="post">
      <input type="text" name="name" placeholder="姓名" />
      <select name="gender">
      <option value="">性别</option>
      <option value="1"></option>
      <option value="2"></option>
      </select>
      <select name="job">
      <option value="">职位</option>
      <option value="1">班主任</option>
      <option value="2">讲师</option>
      <option value="3">学工主管</option>
      <option value="4">教研主管</option>
      <option value="5">咨询师</option>
      </select>
      <button type="submit">查询</button>
      <button type="reset" class="clear">清空</button>
      </form>

      <table class="table table-striped table-bordered">
      <thead>
      <tr>
      <th>姓名</th>
      <th>性别</th>
      <th>头像</th>
      <th>职位</th>
      <th>入职日期</th>
      <th>最后操作时间</th>
      <th>操作</th>
      </tr>
      </thead>
      <tbody>
      <tr>
      <td>令狐冲</td>
      <td></td>
      <td><img src="https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg" alt="令狐冲" class="avatar"></td>
      <td>讲师</td>
      <td>2021-03-15</td>
      <td>2023-07-30T12:00:00Z</td>
      <td class="btn-group">
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
      </td>
      </tr>
      <tr>
      <td>任盈盈</td>
      <td></td>
      <td><img src="https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg" alt="任盈盈" class="avatar"></td>
      <td>学工主管</td>
      <td>2020-04-10</td>
      <td>2023-07-29T15:00:00Z</td>
      <td class="btn-group">
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
      </td>
      </tr>
      <tr>
      <td>岳不群</td>
      <td></td>
      <td><img src="https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg" alt="岳不群" class="avatar"></td>
      <td>教研主管</td>
      <td>2019-01-01</td>
      <td>2023-07-30T10:00:00Z</td>
      <td class="btn-group">
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
      </td>
      </tr>
      <tr>
      <td>宁中则</td>
      <td></td>
      <td><img src="https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg" alt="宁中则" class="avatar"></td>
      <td>班主任</td>
      <td>2018-06-01</td>
      <td>2023-07-29T09:00:00Z</td>
      <td class="btn-group">
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
      </td>
      </tr>
      <tr>
      <td>令狐冲</td>
      <td></td>
      <td><img src="https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg" alt="令狐冲" class="avatar"></td>
      <td>讲师</td>
      <td>2021-03-15</td>
      <td>2023-07-30T12:00:00Z</td>
      <td class="btn-group">
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
      </td>
      </tr>
      <tr>
      <td>任盈盈</td>
      <td></td>
      <td><img src="https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg" alt="任盈盈" class="avatar"></td>
      <td>学工主管</td>
      <td>2020-04-10</td>
      <td>2023-07-29T15:00:00Z</td>
      <td class="btn-group">
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
      </td>
      </tr>
      <tr>
      <td>岳不群</td>
      <td></td>
      <td><img src="https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg" alt="岳不群" class="avatar"></td>
      <td>教研主管</td>
      <td>2019-01-01</td>
      <td>2023-07-30T10:00:00Z</td>
      <td class="btn-group">
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
      </td>
      </tr>
      <tr>
      <td>宁中则</td>
      <td></td>
      <td><img src="https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg" alt="宁中则" class="avatar"></td>
      <td>班主任</td>
      <td>2018-06-01</td>
      <td>2023-07-29T09:00:00Z</td>
      <td class="btn-group">
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
      </td>
      </tr>
      <tr>
      <td>令狐冲</td>
      <td></td>
      <td><img src="https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg" alt="令狐冲" class="avatar"></td>
      <td>讲师</td>
      <td>2021-03-15</td>
      <td>2023-07-30T12:00:00Z</td>
      <td class="btn-group">
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
      </td>
      </tr>
      <tr>
      <td>任盈盈</td>
      <td></td>
      <td><img src="https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg" alt="任盈盈" class="avatar"></td>
      <td>学工主管</td>
      <td>2020-04-10</td>
      <td>2023-07-29T15:00:00Z</td>
      <td class="btn-group">
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
      </td>
      </tr>
      <tr>
      <td>岳不群</td>
      <td></td>
      <td><img src="https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg" alt="岳不群" class="avatar"></td>
      <td>教研主管</td>
      <td>2019-01-01</td>
      <td>2023-07-30T10:00:00Z</td>
      <td class="btn-group">
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
      </td>
      </tr>
      <tr>
      <td>宁中则</td>
      <td></td>
      <td><img src="https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg" alt="宁中则" class="avatar"></td>
      <td>班主任</td>
      <td>2018-06-01</td>
      <td>2023-07-29T09:00:00Z</td>
      <td class="btn-group">
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
      </td>
      </tr>
      </tbody>
      </table>

      <!-- 页脚版权区域 -->
      <footer class="footer">
      <p class="company-name">江苏传智播客教育科技股份有限公司</p>
      <p class="copyright">版权所有 Copyright 2006-2024 All Rights Reserved</p>
      </footer>

      <script>
      //点击列表中每一条记录后面的删除按钮,弹出一个提示框,提示:您是否要删除这条记录? 如果确定,则移出这条记录
      const deleteButtons = document.querySelectorAll('.delete');
      for (let i = 0; i < deleteButtons.length; i++) {
      deleteButtons[i].addEventListener('click', function () {
      if (confirm('您是否要删除这条记录?')) {
      //点击确定按钮,删除当前记录
      this.parentNode.parentNode.remove();
      }
      });
      }

      //通过js实现鼠标移入移出效果,鼠标移入,背景色为 #f2e2e2,鼠标移出,背景色为 白色。
      const trs = document.querySelectorAll('tr');
      for (let i = 1; i < trs.length; i++) {
      trs[i].addEventListener('mouseenter', function () {
      this.style.backgroundColor = '#f2e2e2';
      });
      trs[i].addEventListener('mouseleave', function () {
      this.style.backgroundColor = '#fff';
      });
      }
      </script>

      </div>

      </body>
      </html>

      常见事件

      上面案例中使用到了事件 clickmouseentermouseleave,那都有哪些事件类型供我们使用呢?下面就给大家列举一些比较常用的事件属性:

      示例演示:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      <!DOCTYPE html>
      <html lang="en">

      <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>JS-事件-常见事件</title>
      </head>

      <body>
      <form action="" style="text-align: center;">
      <input type="text" name="username" id="username">
      <input type="text" name="age" id="age">
      <input id="b1" type="submit" value="提交">
      <input id="b2" type="button" value="单击事件">
      </form>

      <br><br><br>

      <table width="800px" border="1" cellspacing="0" align="center">
      <tr>
      <th>学号</th>
      <th>姓名</th>
      <th>分数</th>
      <th>评语</th>
      </tr>
      <tr align="center">
      <td>001</td>
      <td>张三</td>
      <td>90</td>
      <td>很优秀</td>
      </tr>
      <tr align="center" id="last">
      <td>002</td>
      <td>李四</td>
      <td>92</td>
      <td>优秀</td>
      </tr>
      </table>

      <script>
      //click: 鼠标点击事件
      document.querySelector('#b2').addEventListener('click', () => {
      console.log("我被点击了...");
      })

      //mouseenter: 鼠标移入
      document.querySelector('#last').addEventListener('mouseenter', () => {
      console.log("鼠标移入了...");
      })

      //mouseleave: 鼠标移出
      document.querySelector('#last').addEventListener('mouseleave', () => {
      console.log("鼠标移出了...");
      })

      //keydown: 某个键盘的键被按下
      document.querySelector('#username').addEventListener('keydown', () => {
      console.log("键盘被按下了...");
      })

      //keydown: 某个键盘的键被抬起
      document.querySelector('#username').addEventListener('keyup', () => {
      console.log("键盘被抬起了...");
      })

      //blur: 失去焦点事件
      document.querySelector('#age').addEventListener('blur', () => {
      console.log("失去焦点...");
      })

      //focus: 元素获得焦点
      document.querySelector('#age').addEventListener('focus', () => {
      console.log("获得焦点...");
      })

      //input: 用户输入时触发
      document.querySelector('#age').addEventListener('input', () => {
      console.log("用户输入时触发...");
      })

      //submit: 提交表单事件
      document.querySelector('form').addEventListener('submit', () => {
      alert("表单被提交了...");
      })
      </script>
      </body>

      </html>

      前面两天,我们已经学习了前端网页开发的三剑客:HTML、CSS、JS。那通过这三种技术呢,我们就可以开发出一个网页程序了,但是如果我们使用原生的 JS 来处理界面的交互行为,开发效率呢,是比较低的。而在现在的企业项目开发中,一般会借助于 Vue 这样的 js 框架来简化操作、提高开发效率。 那么我们今天接下来呢,就来学习 Vue 这个框架。

      Vue 介绍

      概述

      Vue(读音 /vjuː /, 类似于 view),是一款用于构建用户界面渐进式的 JavaScript 框架(官方网站:https://cn.vuejs.org)。

      在上面的这句话中呢,出现了三个词,分别是:构建用户界面、渐进式、框架

      1). 构建用户界面

      构建用户界面是指,在 Vue 中,可以基于数据渲染出用户看到的界面。 那这句话什么意思呢?我们来举一个例子,比如将来服务器端返回给前端的原始数据呢,就是如下这个样子:

      1
      2
      3
      4
      userList: [
      {"id": 1, "name": "谢逊", "image": "1.jpg", "gender": 1, "job": "班主任"},
      {"id": 2, "name": "韦一笑", "image": "2.jpg", "gender": 1, "job": "班主任"}
      ]

      而上面的这些原始数据,用户是看不懂的。 而我们开发人员呢,可以使用 Vue 中提供的操作,将原始数据遍历、解析出来,从而渲染呈现出用户所能看懂的界面,如下所示:

      那这个过程呢,就是基于数据渲染出用户看到的界面,也就是所谓的 构建用户界面。

      **2). **渐进式

      渐进式中的渐进呢,字面意思就是 “循序渐进”。Vue 生态中的语法呢是非常多的,比如声明式渲染、组件系统、客户端路由(VueRouter)、状态管理(Vuex、Pinia)、构建工具(Webpack、Vite)等等。

      所谓渐进,指的是我们使用 Vue 框架呢,我们不需要把所有的组件、语法全部学习完毕才可以使用 Vue。 而是,我们学习一点就可以使用一点了,比如:

      • 我们学习了声明式渲染,我们就可以使用 Vue 来构建用户界面了。
      • 我们再学习了组件系统,我们就可以使用 Vue 中的组件,从而来复用了。
      • 我们再学习了路由 VueRouter,就可以使用 Vue 中的中的路由功能了。

      也就是说,并不需要全部学习完毕就可以直接使用 Vue 进行开发,简化操作、提高效率了。 Vue 是一个框架,但其实也是一个生态。

      那由此呢,也就引出了 Vue 中两种常见的开发模式:

      • 基于 Vue 提供的核心包,完成项目局部模块的改造了。
      • 基于 Vue 提供的核心包、插件进行工程化开发,也就是做整站开发。

      那上面的这两种 Vue 的使用形式,我们都会学习,今天我们先来学习第一种方式,就是使用 Vue 来完成局部模块改造。

      3).** 框架**

      • 框架:就是一套完整的项目解决方案,用于快速构建项目 。这是我们接触的第一个框架,那在我们后面的学习中,我们还会学习很多的 java 语言中的框架,那通过这些框架呢,就可以来快速开发 java 项目,提高开发效率。
      • 优点:大大提升前端项目的开发效率 。
      • 缺点:需要理解记忆框架的使用规则 。(参照官网)

      好,那我们知道了什么是 Vue 之后,接下来,就要正式进入 Vue 的学习,我们今天主要讲解以下几个方面:

      1. Vue 快速入门
      2. Vue 常用指令
      3. Ajax
      4. Vue 生命周期

      入门程序

      需求

      在入门程序中,最终我们需要将准备的数据 message 的值,基于 Vue 渲染展示在页面中,最终呈现的形式如下:

      步骤

      1). 准备工作:

      • 准备一个 html 文件,并在其中引入 Vue 模块 (参考官方文档,复制过来即可)【注意:模块化的 js,引入时,需要设置 type="module"
      • 创建 Vue 程序的应用实例,控制视图的元素
      • 准备元素(div),交给 Vue 控制

      这是三步准备工作,是我们使用 Vue 时,都需要做的,是固定步骤。 这样我们就搭建好了一个基本的 Vue 的结构了。

      2). 数据驱动视图:

      • 准备数据。 在创建 Vue 应用实例的时候,传入了一个 js 对象,在这个 js 对象中,我们要定义一个 data 方法,这个 data 方法的返回值就是 Vue 中的数据。

      • 通过插值表达式渲染页面。 插值表达式的写法:{{ ... }}

      实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Vue-快速入门</title>
      </head>
      <body>
      <div id="app">
      {{message}}
      </div>

      <script type="module">
      import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
      createApp({
      data(){
      return {
      message: 'Hello Vue'
      }
      }
      }).mount('#app')
      </script>
      </body>
      </html>

      在上述入门程序编写时,需要注意这么几点:

      • Vue 中定义数据,必须通过 data 方法来定义,data 方法返回值是一个对象,在这个对象中定义数据。
      • 插值表达式中编写的变量,一定是 Vue 中定义的数据,如果插值表达式中编写了一个变量,但是在 Vue 中未定义,将会报错 。
      • Vue 应用实例接管的区域是 ‘#app’,超出这个范围,就不受 Vue 控制了,所以 vue 的插值表达式,一定写在 <div id="app">...</div> 的里面 。

      Vue 指令

      概述

      **指令:**指的是 HTML 标签上带有 v- 前缀的特殊属性,不同指令具有不同含义,可以实现不同的功能 。例如:v-if,v-for…

      形式:

      常见指令:

      image-20260604175829217

      案例

      基本实现

      需求:员工列表数据渲染展示 。(备注:基础代码在资料中,已经提供,导入 VsCode 即可)

      [!TIP]
      AI 提示词(prompt):
      基于 Vue3 中提供的指令,渲染展示 employees 中的数据,并展示在页面上;

      • 性别 gender 为 1 展示为男,性别为 2 展示为女;
      • 职位 job 为 1 展示为班主任,职位为 2 展示为讲师,职位为 3 展示为学工主管,职位为 4 展示为教研主管,职位为 5 展示为咨询师;
      • 其他的属性直接展示;
        页面的内容如下:

      生成后的代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      <!DOCTYPE html>
      <html lang="zh-CN">
      <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Tlias智能学习辅助系统</title>
      <style>
      body {
      margin: 0;
      }

      /* 顶栏样式 */
      .header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      background-color: #c2c0c0;
      padding: 20px 20px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
      }

      /* 加大加粗标题 */
      .header h1 {
      margin: 0;
      font-size: 24px;
      font-weight: bold;
      }

      /* 文本链接样式 */
      .header a {
      text-decoration: none;
      color: #333;
      font-size: 16px;
      }

      /* 搜索表单区域 */
      .search-form {
      display: flex;
      align-items: center;
      padding: 20px;
      background-color: #f9f9f9;
      }

      /* 表单控件样式 */
      .search-form input[type="text"], .search-form select {
      margin-right: 10px;
      padding: 10px 10px;
      border: 1px solid #ccc;
      border-radius: 4px;
      width: 26%;
      }

      /* 按钮样式 */
      .search-form button {
      padding: 10px 15px;
      margin-left: 10px;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      }

      /* 清空按钮样式 */
      .search-form button.clear {
      background-color: #6c757d;
      }

      .table {
      min-width: 100%;
      border-collapse: collapse;
      }

      /* 设置表格单元格边框 */
      .table td, .table th {
      border: 1px solid #ddd;
      padding: 8px;
      text-align: center;
      }

      .avatar {
      width: 30px;
      height: 30px;
      object-fit: cover;
      border-radius: 50%;
      }

      /* 页脚版权区域 */
      .footer {
      background-color: #c2c0c0;
      color: white;
      text-align: center;
      padding: 10px 0;
      margin-top: 30px;
      }

      .footer .company-name {
      font-size: 1.1em;
      font-weight: bold;
      }

      .footer .copyright {
      font-size: 0.9em;
      }

      #container {
      width: 80%;
      margin: 0 auto;
      }
      </style>
      </head>
      <body>

      <div id="container">
      <!-- 顶栏 -->
      <div class="header">
      <h1>Tlias智能学习辅助系统</h1>
      <a href="#">退出登录</a>
      </div>

      <!-- 搜索表单区域 -->
      <form class="search-form" action="#" method="post">
      <input type="text" name="name" placeholder="姓名" />
      <select name="gender">
      <option value="">性别</option>
      <option value="1"></option>
      <option value="2"></option>
      </select>
      <select name="job">
      <option value="">职位</option>
      <option value="1">班主任</option>
      <option value="2">讲师</option>
      <option value="3">学工主管</option>
      <option value="4">教研主管</option>
      <option value="5">咨询师</option>
      </select>
      <button type="submit">查询</button>
      <button type="reset" class="clear">清空</button>
      </form>

      <table class="table table-striped table-bordered">
      <thead>
      <tr>
      <th>姓名</th>
      <th>性别</th>
      <th>头像</th>
      <th>职位</th>
      <th>入职日期</th>
      <th>最后操作时间</th>
      <th>操作</th>
      </tr>
      </thead>
      <tbody>
      <tr v-for="(emp, index) in empList" :key="index">
      <td>{{ emp.name }}</td>
      <td>{{ emp.gender === 1 ? '男' : '女' }}</td>
      <td><img :src="emp.image" alt="{{ emp.name }}" class="avatar"></td>
      <td>{{ emp.job === '1' ? '班主任' : (emp.job === '2' ? '讲师' : (emp.job === '3' ? '学工主管' : (emp.job === '4' ? '教研主管' : (emp.job === '5' ? '咨询师' : '未知')))) }}</td>
      <td>{{ emp.entrydate }}</td>
      <td>{{ emp.updatetime }}</td>
      <td class="btn-group">
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
      </td>
      </tr>
      </tbody>
      </table>

      <!-- 页脚版权区域 -->
      <footer class="footer">
      <p class="company-name">江苏传智播客教育科技股份有限公司</p>
      <p class="copyright">版权所有 Copyright 2006-2024 All Rights Reserved</p>
      </footer>

      <script type="module">
      import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
      createApp({
      data() {
      return {
      empList: [
      { "id": 1,
      "name": "谢逊",
      "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/4.jpg",
      "gender": 1,
      "job": "1",
      "entrydate": "2023-06-09",
      "updatetime": "2024-07-30T14:59:38"
      },
      {
      "id": 2,
      "name": "韦一笑",
      "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg",
      "gender": 1,
      "job": "1",
      "entrydate": "2020-05-09",
      "updatetime": "2023-07-01T00:00:00"
      },
      {
      "id": 3,
      "name": "黛绮丝",
      "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/2.jpg",
      "gender": 2,
      "job": "2",
      "entrydate": "2021-06-01",
      "updatetime": "2023-07-01T00:00:00"
      }
      ]
      }
      }
      }).mount('#container')
      </script>

      </div>

      </body>
      </html>

      最终页面展示效果:

      那我们看到,通过 AI 生成的代码,已经实现了员工列表数据渲染的功能。 而这里面呢,就用到了 Vue 中提供的指令和插值表达式。 那接下来,我们就来详细剖析一下 Vue 中指令的用法。

      指令详解

      v-for

      **作用:**列表渲染,遍历容器的元素或者对象的属性

      语法:<tr v-for="(item,index) in items" :key="item.id">{{item}}</tr>

      参数:

      • items 为遍历的数组
      • item 为遍历出来的元素
      • index 为索引/下标,从 0 开始 ;可以省略,省略 index 语法: v-for = "item in items"

      key:

      • 作用:给元素添加的唯一标识,便于 vue 进行列表项的正确排序复用,提升渲染性能
      • 推荐使用 id 作为 key(唯一),不推荐使用 index 作为 key(会变化,不对应)

      注意:遍历的数组,必须在 data 中定义; 要想让哪个标签循环展示多次,就在哪个标签上使用 v-for 指令。

      插值表达式不能出现在标签内部,可以使用v-bind绑定标签内部属性值

      v-bind

      作用:动态为 HTML 标签绑定属性值,如设置 href,src,style 样式等。

      语法:v-bind:属性名="属性值" <img src="/2025/06/04/前端 Web 开发/undefined" `` width="30px">

      简化::属性名="属性值" <img src="/2025/06/04/前端 Web 开发/undefined" width="30px">

      注意:v-bind 所绑定的数据,必须在 data 中定义/或基于 data 中定义的数据而来。

      image-20260604181019548

      image-20260604181135286

      v-if & v-show

      **作用:**这两类指令,都是用来控制元素的显示与隐藏的

      v-if:

      • 语法:v-if=“表达式”,表达式值为 true,显示;false,隐藏
      • 原理:基于条件判断,来控制创建或移除元素节点(条件渲染)
      • 场景:要么显示,要么不显示,不频繁切换的场景
      • 其它:可以配合 v-else-if / v-else 进行链式调用条件判断

      示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      <!-- 基于v-if/v-else-if/v-else指令来展示职位这一列 -->
      <td>
      <span v-if="emp.job === '1'">班主任</span>
      <span v-else-if="emp.job === '2'">讲师</span>
      <span v-else-if="emp.job === '3'">学工主管</span>
      <span v-else-if="emp.job === '4'">教研主管</span>
      <span v-else-if="emp.job === '5'">咨询师</span>
      <span v-else>其他</span>
      </td>

      **注意:v-else-if 必须出现在 v-if 之后,可以出现多个; v-else 必须出现在 v-if/v-else-if 之后 。 **

      通过浏览器的开发者工具,我们可以看到如果使用 v-if 指令来渲染展示,确实是根据条件判断,是否渲染这个元素节点,条件成立才会渲染。查看结果如下:

      v-show:

      • 语法:v-show=“表达式”,表达式值为 true,显示;false,隐藏
      • 原理:基于 CSS 样式 display 来控制显示与隐藏
      • 场景:频繁切换显示隐藏的场景

      示例:

      1
      2
      3
      4
      5
      6
      7
      8
      <!-- 基于v-show指令来展示职位这一列 -->
      <td>
      <span v-show="emp.job === '1'">班主任</span>
      <span v-show="emp.job === '2'">讲师</span>
      <span v-show="emp.job === '3'">学工主管</span>
      <span v-show="emp.job === '4'">教研主管</span>
      <span v-show="emp.job === '5'">咨询师</span>
      </td>

      通过浏览器的开发者工具,我们可以看到如果使用 v-show 指令来渲染展示,所有元素都会渲染,只不过是通过控制 display 这个 css 样式,来决定元素是展示还是隐藏。查看结果如下:

      v-model
      • 作用:在表单元素上使用,双向数据绑定。可以方便的 获取设置 表单项数据(采集用户输入的信息)
      • 语法:v-model="变量名"
      • 这里的双向数据绑定,是指 Vue 中的数据变化,会影响视图中的数据展示 。 视图中的输入的数据变化,也会影响 Vue 的数据模型 。

      注意:v-model 中绑定的变量,必须在 data 中定义。

      • 为员工列表案例的搜索栏的表单项,绑定数据:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      <!DOCTYPE html>
      <html lang="zh-CN">
      <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Tlias智能学习辅助系统</title>
      <style>
      body {
      margin: 0;
      }

      /* 顶栏样式 */
      .header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      background-color: #c2c0c0;
      padding: 20px 20px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
      }

      /* 加大加粗标题 */
      .header h1 {
      margin: 0;
      font-size: 24px;
      font-weight: bold;
      }

      /* 文本链接样式 */
      .header a {
      text-decoration: none;
      color: #333;
      font-size: 16px;
      }

      /* 搜索表单区域 */
      .search-form {
      display: flex;
      align-items: center;
      padding: 20px;
      background-color: #f9f9f9;
      }

      /* 表单控件样式 */
      .search-form input[type="text"], .search-form select {
      margin-right: 10px;
      padding: 10px 10px;
      border: 1px solid #ccc;
      border-radius: 4px;
      width: 26%;
      }

      /* 按钮样式 */
      .search-form button {
      padding: 10px 15px;
      margin-left: 10px;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      }

      /* 清空按钮样式 */
      .search-form button.clear {
      background-color: #6c757d;
      }

      .table {
      min-width: 100%;
      border-collapse: collapse;
      }

      /* 设置表格单元格边框 */
      .table td, .table th {
      border: 1px solid #ddd;
      padding: 8px;
      text-align: center;
      }

      .avatar {
      width: 30px;
      height: 30px;
      object-fit: cover;
      border-radius: 50%;
      }

      /* 页脚版权区域 */
      .footer {
      background-color: #c2c0c0;
      color: white;
      text-align: center;
      padding: 10px 0;
      margin-top: 30px;
      }

      .footer .company-name {
      font-size: 1.1em;
      font-weight: bold;
      }

      .footer .copyright {
      font-size: 0.9em;
      }

      #container {
      width: 80%;
      margin: 0 auto;
      }
      </style>
      </head>
      <body>

      <div id="container">
      <!-- 顶栏 -->
      <div class="header">
      <h1>Tlias智能学习辅助系统</h1>
      <a href="#">退出登录</a>
      </div>

      <!-- 搜索表单区域 -->
      <form class="search-form" action="#" method="post">
      <input type="text" name="name" placeholder="姓名" v-model="searchEmp.name" />
      <select name="gender" v-model="searchEmp.gender">
      <option value="">性别</option>
      <option value="1"></option>
      <option value="2"></option>
      </select>
      <select name="job" v-model="searchEmp.job">
      <option value="">职位</option>
      <option value="1">班主任</option>
      <option value="2">讲师</option>
      <option value="3">学工主管</option>
      <option value="4">教研主管</option>
      <option value="5">咨询师</option>
      </select>
      <button type="submit">查询</button>
      <button type="reset" class="clear">清空</button>
      </form>

      <table class="table table-striped table-bordered">
      <thead>
      <tr>
      <th>姓名</th>
      <th>性别</th>
      <th>头像</th>
      <th>职位</th>
      <th>入职日期</th>
      <th>最后操作时间</th>
      <th>操作</th>
      </tr>
      </thead>
      <tbody>
      <tr v-for="(emp, index) in empList" :key="index">
      <td>{{ emp.name }}</td>
      <td>{{ emp.gender === 1 ? '男' : '女' }}</td>
      <td><img :src="emp.image" class="avatar"></td>
      <td>
      <span v-if="emp.job === '1'">班主任</span>
      <span v-else-if="emp.job === '2'">讲师</span>
      <span v-else-if="emp.job === '3'">学工主管</span>
      <span v-else-if="emp.job === '4'">教研主管</span>
      <span v-else-if="emp.job === '5'">咨询师</span>
      </td>
      <td>{{ emp.entrydate }}</td>
      <td>{{ emp.updatetime }}</td>
      <td class="btn-group">
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
      </td>
      </tr>
      </tbody>
      </table>

      <!-- 页脚版权区域 -->
      <footer class="footer">
      <p class="company-name">江苏传智播客教育科技股份有限公司</p>
      <p class="copyright">版权所有 Copyright 2006-2024 All Rights Reserved</p>
      </footer>

      <script type="module">
      import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
      createApp({
      data() {
      return {
      searchEmp: {
      name: '',
      gender: '',
      job: ''
      },
      empList: [
      { "id": 1,
      "name": "谢逊",
      "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/4.jpg",
      "gender": 1,
      "job": "1",
      "entrydate": "2023-06-09",
      "updatetime": "2024-07-30T14:59:38"
      },
      {
      "id": 2,
      "name": "韦一笑",
      "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg",
      "gender": 1,
      "job": "1",
      "entrydate": "2020-05-09",
      "updatetime": "2023-07-01T00:00:00"
      },
      {
      "id": 3,
      "name": "黛绮丝",
      "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/2.jpg",
      "gender": 2,
      "job": "2",
      "entrydate": "2021-06-01",
      "updatetime": "2023-07-01T00:00:00"
      }
      ]
      }
      }
      }).mount('#container')
      </script>

      </div>

      </body>
      </html>
      v-on

      作用:为 html 标签绑定事件(添加时间监听)

      语法:

      • v-on:事件名="方法名"
      • 简写为 @事件名="…"
      • <input type="button" value="点我一下试试" v-on:click="handle">
      • <input type="button" value="点我一下试试" @click="handle">

      这里的 handle 函数,就需要在 Vue 应用实例创建的时候创建出来,在 methods 定义。

      • 为员工列表案例的搜索栏中的查询 和 清空按钮绑定事件:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      <!DOCTYPE html>
      <html lang="zh-CN">
      <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Tlias智能学习辅助系统</title>
      <style>
      body {
      margin: 0;
      }

      /* 顶栏样式 */
      .header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      background-color: #c2c0c0;
      padding: 20px 20px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
      }

      /* 加大加粗标题 */
      .header h1 {
      margin: 0;
      font-size: 24px;
      font-weight: bold;
      }

      /* 文本链接样式 */
      .header a {
      text-decoration: none;
      color: #333;
      font-size: 16px;
      }

      /* 搜索表单区域 */
      .search-form {
      display: flex;
      align-items: center;
      padding: 20px;
      background-color: #f9f9f9;
      }

      /* 表单控件样式 */
      .search-form input[type="text"], .search-form select {
      margin-right: 10px;
      padding: 10px 10px;
      border: 1px solid #ccc;
      border-radius: 4px;
      width: 26%;
      }

      /* 按钮样式 */
      .search-form button {
      padding: 10px 15px;
      margin-left: 10px;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      }

      /* 清空按钮样式 */
      .search-form button.clear {
      background-color: #6c757d;
      }

      .table {
      min-width: 100%;
      border-collapse: collapse;
      }

      /* 设置表格单元格边框 */
      .table td, .table th {
      border: 1px solid #ddd;
      padding: 8px;
      text-align: center;
      }

      .avatar {
      width: 30px;
      height: 30px;
      object-fit: cover;
      border-radius: 50%;
      }

      /* 页脚版权区域 */
      .footer {
      background-color: #c2c0c0;
      color: white;
      text-align: center;
      padding: 10px 0;
      margin-top: 30px;
      }

      .footer .company-name {
      font-size: 1.1em;
      font-weight: bold;
      }

      .footer .copyright {
      font-size: 0.9em;
      }

      #container {
      width: 80%;
      margin: 0 auto;
      }
      </style>
      </head>
      <body>

      <div id="container">
      <!-- 顶栏 -->
      <div class="header">
      <h1>Tlias智能学习辅助系统</h1>
      <a href="#">退出登录</a>
      </div>

      <!-- 搜索表单区域 -->
      <form class="search-form">
      <input type="text" name="name" placeholder="姓名" v-model="searchEmp.name" />
      <select name="gender" v-model="searchEmp.gender">
      <option value="">性别</option>
      <option value="1"></option>
      <option value="2"></option>
      </select>
      <select name="job" v-model="searchEmp.job">
      <option value="">职位</option>
      <option value="1">班主任</option>
      <option value="2">讲师</option>
      <option value="3">学工主管</option>
      <option value="4">教研主管</option>
      <option value="5">咨询师</option>
      </select>
      <button type="button" @click="search">查询</button>
      <button type="button" @click="clear">清空</button>
      </form>

      <table class="table table-striped table-bordered">
      <thead>
      <tr>
      <th>姓名</th>
      <th>性别</th>
      <th>头像</th>
      <th>职位</th>
      <th>入职日期</th>
      <th>最后操作时间</th>
      <th>操作</th>
      </tr>
      </thead>
      <tbody>
      <tr v-for="(emp, index) in empList" :key="index">
      <td>{{ emp.name }}</td>
      <td>{{ emp.gender === 1 ? '男' : '女' }}</td>
      <td><img :src="emp.image" alt="{{ emp.name }}" class="avatar"></td>
      <td>
      <span v-if="emp.job === '1'">班主任</span>
      <span v-else-if="emp.job === '2'">讲师</span>
      <span v-else-if="emp.job === '3'">学工主管</span>
      <span v-else-if="emp.job === '4'">教研主管</span>
      <span v-else-if="emp.job === '5'">咨询师</span>
      </td>
      <td>{{ emp.entrydate }}</td>
      <td>{{ emp.updatetime }}</td>
      <td class="btn-group">
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
      </td>
      </tr>
      </tbody>
      </table>

      <!-- 页脚版权区域 -->
      <footer class="footer">
      <p class="company-name">江苏传智播客教育科技股份有限公司</p>
      <p class="copyright">版权所有 Copyright 2006-2024 All Rights Reserved</p>
      </footer>

      <script type="module">
      import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
      createApp({
      data() {
      return {
      searchEmp: {
      name: '',
      gender: '',
      job: ''
      },
      empList: [
      { "id": 1,
      "name": "谢逊",
      "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/4.jpg",
      "gender": 1,
      "job": "1",
      "entrydate": "2023-06-09",
      "updatetime": "2024-07-30T14:59:38"
      },
      {
      "id": 2,
      "name": "韦一笑",
      "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/1.jpg",
      "gender": 1,
      "job": "1",
      "entrydate": "2020-05-09",
      "updatetime": "2023-07-01T00:00:00"
      },
      {
      "id": 3,
      "name": "黛绮丝",
      "image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2023/2.jpg",
      "gender": 2,
      "job": "2",
      "entrydate": "2021-06-01",
      "updatetime": "2023-07-01T00:00:00"
      }
      ]
      }
      },
      methods: {
      search() {
      console.log(this.searchEmp)
      },
      clear() {
      this.searchEmp = {
      name: '',
      gender: '',
      job: ''
      }
      }
      }
      }).mount('#container')
      </script>

      </div>

      </body>
      </html>

      注意: methods 函数中的 this 指向 Vue 实例,可以通过 this 获取到 data 中定义的数据。

      image-20260604182659032

      Ajax

      概述

      我们前端页面中的数据,如下图所示的表格中的员工信息,应该来自于后台,那么我们的后台和前端是互不影响的 2 个程序,那么我们前端应该如何从后台获取数据呢?因为是 2 个程序,所以必须涉及到 2 个程序的交互,所以这就需要用到我们接下来学习的 Ajax 技术。

      Ajax: 全称 Asynchronous JavaScript And XML,异步的 JavaScript 和 XML。其作用有如下 2 点:

      • 与服务器进行数据交换:通过 Ajax 可以给服务器发送请求,并获取服务器响应的数据。

      • 异步交互:可以在不重新加载整个页面的情况下,与服务器交换数据并更新部分网页的技术,如:搜索联想、用户名是否可用的校验等等。

        image-20260604182841015

      我们详细的解释一下 Ajax 技术的 2 个作用:

      • 与服务器进行数据交互

      如下图所示前端资源被浏览器解析,但是前端页面上缺少数据,前端可以通过 Ajax 技术,向后台服务器发起请求,后台服务器接受到前端的请求,从数据库中获取前端需要的资源,然后响应给前端,前端在通过我们学习的 vue 技术,可以将数据展示到页面上,这样用户就能看到完整的页面了。此处可以对比 JavaSE 中的网络编程技术来理解。

      • 异步交互:可以在不重新加载整个页面的情况下,与服务器交换数据并更新部分网页的技术。

      如下图所示,当我们再百度搜索 java 时,下面的联想数据是通过 Ajax 请求从后台服务器得到的,在整个过程中,我们的 Ajax 请求不会导致整个百度页面的重新加载,并且只针对搜索栏这局部模块的数据进行了数据的更新,不会对整个页面的其他地方进行数据的更新,这样就大大提升了页面的加载速度,用户体验高。

      XML:(英语:Extensible Markup Language)可扩展标记语言,本质是一种数据格式,可以用来存储复杂的数据结构。

      同步异步

      针对于上述 Ajax 的局部刷新功能是因为 Ajax 请求是异步的,与之对应的有同步请求。接下来我们介绍一下异步请求和同步请求的区别。

      • 同步请求发送过程如下图所示:

      浏览器页面在发送请求给服务器,在服务器处理请求的过程中,浏览器页面不能做其他的操作。只能等到服务器响应结束后才能,浏览器页面才能继续做其他的操 作。

      • 异步请求发送过程如下图所示:

      浏览器页面发送请求给服务器,在服务器处理请求的过程中,浏览器页面还可以做其他的操作。

      Axios

      使用原生的 Ajax 请求的代码编写起来还是比较繁琐的,所以接下来我们学习一门更加简单的发送 Ajax 请求的技术 Axios 。Axios 是对原生的 AJAX 进行封装,简化书写。Axios 官网是:https://www.axios-http.cn

      image-20260604183607943

      入门程序

      Axios 的使用比较简单,主要分为 2 步:

      1). 引入 Axios 文件(如果网络不通畅,可以使用离线的已经下载好的 js 文件,资料中已经提供)

      1
      <script src="https://unpkg.com/axios/dist/axios.min.js"></script>

      2). 点击按钮时,使用 Axios 发送请求

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Axios入门程序</title>
      </head>
      <body>

      <button id="getData">GET</button>
      <button id="postData">POST</button>

      <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
      <script>
      //GET请求
      document.querySelector('#getData').onclick = function() {
      axios({
      url:'https://mock.apifox.cn/m1/3083103-0-default/emps/list',
      method:'get'
      }).then(function(res) {
      console.log(res.data);
      }).catch(function(err) {
      console.log(err);
      })
      }

      //POST请求
      document.querySelector('#postData').onclick = function() {
      axios({
      url:'https://mock.apifox.cn/m1/3083103-0-default/emps/update',
      method:'post'
      }).then(function(res) {
      console.log(res.data);
      }).catch(function(err) {
      console.log(err);
      })
      }
      </script>
      </body>
      </html>

      [!TIP]
      知识小贴士:在使用 axios 时,在 axios 之后,输入 thenc 会自动生成成功及失败回调函数结构 。

      请求方法别名

      Axios 还针对不同的请求,提供了别名方式的 api,具体格式如下:

      axios.请求方式(url [, data [, config]])

      [!Tip]

      如果用post请求,传入第二个参数,如果还有一些其他的配置信息,传入第三个参数

      具体如下:

      我们目前只关注 get 和 post 请求,所以在上述的入门案例中,我们可以将 get 请求代码改写成如下:

      1
      2
      3
      axios.get("https://mock.apifox.cn/m1/3083103-0-default/emps/list").then(result => {
      console.log(result.data);
      })

      post 请求改写成如下:

      1
      2
      3
      axios.post("https://mock.apifox.cn/m1/3083103-0-default/emps/update","id=1").then(result => {
      console.log(result.data);
      })

      案例-异步获取数据

      需求:基于 axios 动态加载员工列表数据

      具体代码实现如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      <!DOCTYPE html>
      <html lang="zh-CN">
      <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Tlias智能学习辅助系统</title>
      <style>
      body {
      margin: 0;
      }

      /* 顶栏样式 */
      .header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      background-color: #c2c0c0;
      padding: 20px 20px;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
      }

      /* 加大加粗标题 */
      .header h1 {
      margin: 0;
      font-size: 24px;
      font-weight: bold;
      }

      /* 文本链接样式 */
      .header a {
      text-decoration: none;
      color: #333;
      font-size: 16px;
      }

      /* 搜索表单区域 */
      .search-form {
      display: flex;
      align-items: center;
      padding: 20px;
      background-color: #f9f9f9;
      }

      /* 表单控件样式 */
      .search-form input[type="text"], .search-form select {
      margin-right: 10px;
      padding: 10px 10px;
      border: 1px solid #ccc;
      border-radius: 4px;
      width: 26%;
      }

      /* 按钮样式 */
      .search-form button {
      padding: 10px 15px;
      margin-left: 10px;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      }

      /* 清空按钮样式 */
      .search-form button.clear {
      background-color: #6c757d;
      }

      .table {
      min-width: 100%;
      border-collapse: collapse;
      }

      /* 设置表格单元格边框 */
      .table td, .table th {
      border: 1px solid #ddd;
      padding: 8px;
      text-align: center;
      }

      .avatar {
      width: 30px;
      height: 30px;
      object-fit: cover;
      border-radius: 50%;
      }

      /* 页脚版权区域 */
      .footer {
      background-color: #c2c0c0;
      color: white;
      text-align: center;
      padding: 10px 0;
      margin-top: 30px;
      }

      .footer .company-name {
      font-size: 1.1em;
      font-weight: bold;
      }

      .footer .copyright {
      font-size: 0.9em;
      }

      #container {
      width: 80%;
      margin: 0 auto;
      }
      </style>
      </head>
      <body>

      <div id="container">
      <!-- 顶栏 -->
      <div class="header">
      <h1>Tlias智能学习辅助系统</h1>
      <a href="#">退出登录</a>
      </div>

      <!-- 搜索表单区域 -->
      <form class="search-form">
      <input type="text" name="name" placeholder="姓名" v-model="searchForm.name" />
      <select name="gender" v-model="searchForm.gender">
      <option value="">性别</option>
      <option value="1"></option>
      <option value="2"></option>
      </select>
      <select name="job" v-model="searchForm.job">
      <option value="">职位</option>
      <option value="1">班主任</option>
      <option value="2">讲师</option>
      <option value="3">学工主管</option>
      <option value="4">教研主管</option>
      <option value="5">咨询师</option>
      </select>
      <button type="button" @click="search">查询</button>
      <button type="button" @click="clear">清空</button>
      </form>

      <table class="table table-striped table-bordered">
      <thead>
      <tr>
      <th>姓名</th>
      <th>性别</th>
      <th>头像</th>
      <th>职位</th>
      <th>入职日期</th>
      <th>最后操作时间</th>
      <th>操作</th>
      </tr>
      </thead>
      <tbody>
      <tr v-for="(emp, index) in empList" :key="index">
      <td>{{ emp.name }}</td>
      <td>{{ emp.gender === 1 ? '男' : '女' }}</td>
      <td><img :src="emp.image" alt="{{ emp.name }}" class="avatar"></td>

      <!-- 基于v-if/v-else-if/v-else指令来展示职位这一列 -->
      <td>
      <span v-if="emp.job == '1'">班主任</span>
      <span v-else-if="emp.job == '2'">讲师</span>
      <span v-else-if="emp.job == '3'">学工主管</span>
      <span v-else-if="emp.job == '4'">教研主管</span>
      <span v-else-if="emp.job == '5'">咨询师</span>
      <span v-else>其他</span>
      </td>

      <!-- 基于v-show指令来展示职位这一列 -->
      <!-- <td>
      <span v-show="emp.job === '1'">班主任</span>
      <span v-show="emp.job === '2'">讲师</span>
      <span v-show="emp.job === '3'">学工主管</span>
      <span v-show="emp.job === '4'">教研主管</span>
      <span v-show="emp.job === '5'">咨询师</span>
      </td> -->

      <td>{{ emp.entrydate }}</td>
      <td>{{ emp.updatetime }}</td>
      <td class="btn-group">
      <button class="edit">编辑</button>
      <button class="delete">删除</button>
      </td>
      </tr>
      </tbody>
      </table>

      <!-- 页脚版权区域 -->
      <footer class="footer">
      <p class="company-name">江苏传智播客教育科技股份有限公司</p>
      <p class="copyright">版权所有 Copyright 2006-2024 All Rights Reserved</p>
      </footer>

      <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
      <script type="module">
      import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
      createApp({
      data() {
      return {
      searchForm: {
      name: '',
      gender: '',
      job: ''
      },
      empList: []
      }
      },
      methods: {
      search() {
      //基于axios发送异步请求,请求https://web-server.itheima.net/emps/list,根据条件查询员工列表
      axios.get(`https://web-server.itheima.net/emps/list?name=${this.searchForm.name}&gender=${this.searchForm.gender}&job=${this.searchForm.job}`).then(res => {
      this.empList = res.data.data
      })
      },
      clear() {
      this.searchForm= {
      name: '',
      gender: '',
      job: ''
      }
      }
      }
      }).mount('#container')
      </script>

      </div>

      </body>
      </html>

      如果使用 axios 中提供的.then(function(){…}).catch(function(){…}),这种回调函数的写法,会使得代码的可读性和维护性变差。 而为了解决这个问题,我们可以使用两个关键字,分别是:async、await

      可以通过 async、await 可以让异步变为同步操作。async 就是来声明一个异步方法,await 是用来等待异步任务执行。

      image-20260604184936622

      代码修改前:

      1
      2
      3
      4
      5
      6
      search() {
      //基于axios发送异步请求,请求https://web-server.itheima.net/emps/list,根据条件查询员工列表
      axios.get(`https://web-server.itheima.net/emps/list?name=${this.searchForm.name}&gender=${this.searchForm.gender}&job=${this.searchForm.job}`).then(res => {
      this.empList = res.data.data
      })
      },

      代码修改后:

      1
      2
      3
      4
      5
      async search() {
      //基于axios发送异步请求,请求https://web-server.itheima.net/emps/list,根据条件查询员工列表
      const result = await axios.get(`https://web-server.itheima.net/emps/list?name=${this.searchForm.name}&gender=${this.searchForm.gender}&job=${this.searchForm.job}`);
      this.empList = result.data.data;
      },

      修改后,代码就变成同步操作了,一行一行的从前往后执行。 在前端项目开发中,经常使用这两个关键字配合,使得代码的可读性和可维护性变高。

      Vue 生命周期

      介绍

      vue 的生命周期:指的是 vue 对象从创建到销毁的过程。

      vue 的生命周期包含 8 个阶段:每触发一个生命周期事件,会自动执行一个生命周期方法,这些生命周期方法也被称为钩子方法。其完整的生命周期如下图所示:

      下图是 Vue 官网提供的从创建 Vue 到效果 Vue 对象的整个过程及各个阶段对应的钩子函数:

      其中我们需要重点关注的是 mounted,其他的我们了解即可。

      mounted:挂载完成,Vue 初始化成功,HTML 页面渲染成功。以后我们一般用于页面初始化自动的 ajax 请求后台数据

      image-20260604185326606

      image-20260604185514454

      案例完善

      那我们要想在页面加载完毕,就查询出员工列表,就可以在 mounted 钩子函数中,发送异步请求查询员工数据了。

      具体代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      methods: {
      async search() {
      //基于axios发送异步请求,请求https://web-server.itheima.net/emps/list,根据条件查询员工列表
      const result = await axios.get(`https://web-server.itheima.net/emps/list?name=${this.searchForm.name}&gender=${this.searchForm.gender}&job=${this.searchForm.job}`);
      this.empList = result.data.data;
      },
      clear() {
      this.searchForm= {
      name: '',
      gender: '',
      job: ''
      }
      this.search();
      }
      },
      mounted() {
      this.search();
      }
      }).mount('#container')

      到此,员工列表查询的功能我们就已经完成了。 那关于 Vue 的其他高级用法,我们将在后面的前端 web 实战中来详细讲解。

      附录

      15-前端 Web 实战(Vue 工程化 +ElementPlus)

      [!TIP]
      如果大家在自学的过程中,时间紧迫、没有方向、经常遇到难以解决的 Bug、没有亮眼的项目、缺少 AI 大模型经验,而又想快速系统化的学习,高薪就业的小伙伴儿,大家都可以 🔗直接点我 ,了解一下我们系统化的课程体系。

      Vue 工程化

      前面我们在介绍 Vue 的时候,我们讲到 Vue 是一款用于构建用户界面的渐进式 JavaScript 框架 。(官方:https://cn.vuejs.org/

      那在前面的课程中,我们已经学习了 Vue 的基本语法、表达式、指令,并基于 Vue 的核心包,完成了 Vue 的案例。 那今天呢,我们要来讲解的基于 Vue 进行整站开发。

      介绍

      在前面的课程中,我们学习了 HTML、CSS、JS、Axios、Vue 等技术,并基于完成了一些前端开发的案例 。我们目前的前端开发中,当我们需要使用一些资源时,例如:vue.js,和 axios.js 文件,都是直接再工程中导入的,如下图所示:

      但是上述开发模式存在如下问题:

      • 不规范:每次开发都是从零开始,比较麻烦
      • 难复用:多个页面中的组件共用性不好
      • 难维护:js、图片等资源没有规范化的存储目录,没有统一的标准,不方便维护

      所以现在企业开发中更加讲究前端工程化方式的开发,主要包括如下 4 个特点:

      • 模块化:将 js 和 css 等,做成一个个可复用模块
      • 组件化:我们将 UI 组件,css 样式,js 行为封装成一个个的组件,便于管理
      • 规范化:我们提供一套标准的规范的目录接口和编码规范,所有开发人员遵循这套规范
      • 自动化:项目的构建,测试,部署全部都是自动完成

      所以对于前端工程化,说白了,就是在企业级的前端项目开发中,把前端开发所需要的工具、技术、流程、经验进行规范化和标准化。从而统一开发规范、提升开发效率,降低开发难度、提高复用等等。接下来我们就需要学习 vue 的官方提供的脚手架帮我们完成前端的工程化。

      环境准备

      介绍

      • 介绍:create-vue 是 Vue 官方提供的最新的脚手架工具,用于快速生成一个工程化的 Vue 项目。

      • create-vue 提供了如下功能:

        • 统一的目录结构
        • 本地调试
        • 热部署
        • 单元测试
        • 集成打包上线
      • 而要想使用 create-vue 来创建 vue 项目,则必须安装依赖环境:NodeJS

      NodeJS 安装

      安装包,在课程资料中已经提供了。如下:

      1). 双击 msi 文件,勾选我接受

      **2). 选择安装到一个,****没有中文,没有空格 **的目录下(新建一个文件夹 NodeJS)

      3). 点击 Next,下一步下一步的安装即可。

      4). 验证 NodeJS 的环境变量

      NodeJS 安装完毕后,会自动配置好环境变量,我们验证一下是否安装成功,通过: node -v

      5). 配置 npm 的全局安装路径

      使用 管理员身份** **运行命令行,在命令行中,执行如下指令:

      1
      npm config set prefix "D:\develop\NodeJS"

      注意:D:\develop\NodeJS  这个目录是 NodeJS 的安装目录 !!!

      注意:D:\develop\NodeJS  这个目录是 NodeJS 的安装目录 !!!

      注意:D:\develop\NodeJS  这个目录是 NodeJS 的安装目录 !!!

      6). 切换为淘宝镜像,加速下载:

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

      npm 介绍

      • **npm:**Node Package Manager,是 NodeJS 的软件包管理器。

      在开发前端项目的过程中,我们需要相关的依赖,就可以直接通过 npm install xxx 命令,直接从远程仓库中将依赖直接下载到本地了。

      Vue 项目创建

      项目创建

      image-20260609135058743

      创建一个工程化的 Vue 项目,执行命令:npm create vue``@3.3.4

      详细步骤说明:

      • Project name:------------------》项目名称,默认值:vue-project,可输入想要的项目名称。
      • Add TypeScript? ----------------》是否加入 TypeScript 组件?默认值:No。
      • Add JSX Support? --------------》是否加入 JSX 支持?默认值:No。
      • Add Vue Router...--------------》是否为单页应用程序开发添加 Vue Router 路由管理组件?默认值:No。
      • Add Pinia ...----------------------》是否添加 Pinia 组件来进行状态管理?默认值:No。
      • Add Vitest ...---------------------》是否添加 Vitest 来进行单元测试?默认值:No。
      • Add an End-to-End ...-----------》是否添加端到端测试?默认值 No。
      • Add ESLint for code quality? —》是否添加 ESLint 来进行代码质量检查?默认值:No。

      [!TIP]
      **提示:**执行上述指令,将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具

      项目创建完成以后,进入 vue-project01 项目目录,执行命令安装当前项目的依赖:npm install

      创建项目以及安装依赖的过程,都是需要联网的。【如果网络不太好,可能会造成依赖下载不完整报错,继续再次执行 命令安装。】

      项目结构

      我们可以使用 VsCode 直接打开这个 Vue 项目。

      这是我们创建的第一个项目结构,接下来呢,我们来介绍一下这个项目的结构。如图所示:

      在上述的目录中,我们以后操作的最多的目录,就是 src 目录,因为我们需要在这个目录下来编写前端代码。

      启动项目

      • 方式一:命令行

      启动项目,我们可以在命令行中执行命令:npm run dev,就可以启动 Vue 项目了。

      • 方式二:Vscode 图形化界面

      点击 NPM 脚本中的 dev 后的运行按钮,就可以启动项目。

      启动起来之后,我们就可以访问前端 Vue 项目了,访问路径:http://localhost:5173

      Vue 项目开发流程

      如下图:

      其中 *.vue 是 Vue 项目中的组件文件,在 Vue 项目中也称为单文件组件(SFC,Single-File Components)。Vue 的单文件组件会将一个组件的逻辑 (JS),模板 (HTML) 和样式 (CSS) 封装在同一个文件里(*.vue

      API 风格

      • Vue 的组件有两种不同的风格:组合式 API选项式 API
      • 组合式 API:是 Vue3 提供的一种基于函数的组件编写方式,通过使用函数来组织和复用组件的逻辑。它提供了一种更灵活、更可组合的方式来编写组件。代码形式如下:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      <script setup>
      import { ref, onMounted } from 'vue';
      const count = ref(0); //声明响应式变量

      function increment(){ //声明函数
      count.value++;
      }

      onMounted(() => { //声明钩子函数
      console.log('Vue Mounted....');
      })
      </script>

      <template>
      <input type="button" @click="increment"> Api Demo1 Count : {{ count }}
      </template>

      <style scoped>

      </style>

      [!TIP]

      • setup:是一个标识,告诉 Vue 需要进行一些处理,让我们可以更简洁的使用组合式 API。
      • ref():接收一个内部值,返回一个响应式的 ref 对象,此对象只有一个指向内部值的属性 value。(使用需要导包)
      • onMounted():在组合式 API 中的钩子方法,注册一个回调函数,在组件挂载完成后执行。

      image-20260609174957181

      组合式api中没有this关键字

      image-20260609175343447

      image-20260609175523291

      • 选项式 API

      **选项式 API:**可以用包含多个选项的对象来描述组件的逻辑,如:datamethodsmounted 等。选项定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      <script>
      export default{
      data() {
      return {
      count: 0
      }
      },
      methods: {
      increment: function(){
      this.count++
      }
      },
      mounted() {
      console.log('vue mounted.....');
      }
      }
      </script>

      <template>
      <input type="button" @click="increment">Api Demo1 Count : {{ count }}
      </template>

      <style scoped>

      </style>

      [!TIP]
      在 Vue 中的组合式 API 使用时,是没有 this 对象的,this 对象是 undefined。

      image-20260609174713169

      案例

      在 Vue 项目中,基于组合式 API 完成用户列表数据渲染。 要求:在页面加载完毕之后,发送异步请求,加载数据,渲染表格。

      最终实现效果:

      代码实现如下:

      在 src 下定义 views 目录,在 views 目录中定义文件 UserList.vue, 然后,我们就可以将之前实现的 Vue 案例中的代码,基于 Vue 工程化的形式来实现一遍。 (之前实现的这个页面,在资料中已经提供了:1.工程化-案例-素材.html)

      1). 把原来定义的 CSS 样式代码,拷贝到 <style></style> 标签中。

      2). 把页面的 <div id="app"></div> 中的页面展示的 html 标签代码,拷贝到 <template></template> 标签中。

      3). 将页面的 JS 代码,按照组合式 API 的形式,在 <script></script> 中定义出来。

      4). 而由于在这个案例中,需要用到 axios 来发送异步请求,所以还需要安装 axios

      具体代码如下:

      views/UserList.vue 代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      <script setup>
      //引入ref
      import { ref, onMounted } from 'vue'
      import axios from 'axios'

      //声明数据变量 userList, name, gender, job
      const userList = ref([])
      const name = ref('')
      const gender = ref('')
      const job = ref('')

      //声明函数,基于axios查询数据
      const search = () => {
      //发送请求
      axios.get(`https://web-server.itheima.net/emps/list?name=${name.value}&gender=${gender.value}&job=${job.value}`).then(res => {
      //将查询到的数据赋值给userList
      userList.value = res.data.data
      })
      }

      //定义钩子函数 onMounted
      onMounted(() => {
      //调用search函数
      search()
      })
      </script>

      <template>
      <div id="center">
      姓名: <input type="text" name="name" v-model="name">

      性别:
      <select name="gender" v-model="gender">
      <option value="1"></option>
      <option value="2"></option>
      </select>

      职位:
      <select name="job" v-model="job">
      <option value="1">班主任</option>
      <option value="2">讲师</option>
      <option value="3">其他</option>
      </select>

      <input class="btn" type="button" value="查询" @click="search">
      </div>

      <table>
      <tr>
      <th>序号</th>
      <th>姓名</th>
      <th>头像</th>
      <th>性别</th>
      <th>职位</th>
      <th>入职时间</th>
      <th>更新时间</th>
      </tr>

      <!-- v-for 用于列表循环渲染元素 -->
      <tr v-for="(user, index) in userList" :key="user.id">
      <td>{{index + 1}}</td>
      <td>{{user.name}}</td>
      <td> <img :src="user.image"> </td>
      <td>
      <span v-if="user.gender == 1"></span>
      <span v-else-if="user.gender == 2"></span>
      <span v-else>其他</span>
      </td>
      <td>
      <span v-show="user.job == 1">班主任</span>
      <span v-show="user.job == 2">讲师</span>
      <span v-show="user.job != 1 && user.job != 2">其他</span>
      </td>
      <td>{{user.entrydate}}</td>
      <td>{{user.updatetime}}</td>
      </tr>
      </table>
      </template>

      <style scoped>
      table,th,td {
      border: 1px solid #000;
      border-collapse: collapse;
      line-height: 50px;
      text-align: center;
      }

      #center,table {
      width: 60%;
      margin: auto;
      }

      #center {
      margin-bottom: 20px;
      }

      img {
      width: 50px;
      }

      input,select {
      width: 17%;
      padding: 10px;
      margin-right: 30px;
      border: 1px solid #ccc;
      border-radius: 4px;
      }

      .btn {
      background-color: #ccc;
      }
      </style>

      然后在 App.vue 中,将 UserList.vue 引入进来。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      <script setup>
      import UserList from './views/user/UserList.vue'
      </script>

      <template>
      <UserList></UserList>
      </template>

      <style scoped>

      </style>

      最终展示形式如下:

      ElementPlus

      介绍

      Element:是饿了么公司前端开发团队提供的一套基于 Vue3 的网站组件库,用于快速构建网页。

      Element 提供了很多组件(组成网页的部件)供我们使用。例如 超链接、按钮、图片、表格等等。

      官方网站:https://element-plus.org/zh-CN/#/zh-CN

      如下图所示就是我们开发的页面和 ElementUI 提供的效果对比:可以发现 ElementUI 提供的各式各样好看的按钮。

      ElementPlus 的学习方式和我们之前的学习方式不太一样,对于 ElementPlus,我们作为一个后台开发者,只需要学会如何从 ElementPlus 的官网拷贝组件到我们自己的页面中,并且做一些修改即可。 我们主要学习的是 ElementPlus 中提供的常用组件,至于其他组件同学们可以通过我们这几个组件的学习掌握到 ElementPlus 的学习技巧,然后课后自行学习。

      快速入门

      准备工作:

      1). 将资料中提供的基础工程(资料/04. ElementPlus基础工程/vue-project02.zip),解压到工作目录中,使用 VSCode 将其打开。

      2). 参照官方文档,安装 ElementPlus 的组件库(在当前工程的目录下),执行如下命令:【这一步可以不做,已经安装好了】

      1
      npm install element-plus@2.4.4 --save

      3). 在 main.js 中引入 ElementPlus 组件库 (参照官方文档),最终 main.js 中代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      import { createApp } from 'vue'
      import App from './App.vue'

      import ElementPlus from 'element-plus'
      import 'element-plus/dist/index.css'

      const app = createApp(App)
      app.use(ElementPlus)

      app.mount('#app')

      制作组件:

      1). 访问 ElementPlus 的官方文档,查看对应的组件源代码。

      2). 在 src 下创建 views 目录,在 views 目录下,创建 Element.vue 组件文件,复制组件代码,调整成自己想要的 。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      <script setup>

      </script>

      <template>
      <el-row 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>
      </el-row>
      </template>

      <style scoped>

      </style>

      3). 在根组件 app.vue 中引入 Element.vue

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      <script setup>
      import Element from './views/Element.vue'
      </script>

      <template>
      <Element></Element>
      </template>

      <style scoped>

      </style>

      4). 启动项目,访问 http://localhost:5173

      image-20260609192243943

      常见组件

      表格组件

      用于展示多条结构类似的数据, 可对数据进行排序、筛选、对比或其他自定义操作。

      接下来我们通过代码来演示。

      首先我们需要来到 ElementPlus 的组件库中,找到表格组件,如下图所示:

      然后在 Element.vue 组件中继续制作表格。组件中,需要注意的是,我们组件包括了 3 个部分,如果官方有除了 <template> 部分之外的 <style><script> 都需要复制。具体操作如下图所示:

      整体代码如下所示(绿色背景部分代码为本次增加的代码):

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      <script setup>
      const tableData = [
      {date: '2016-05-03', name: 'Tom', address: 'No. 189, Grove St, Los Angeles'},
      {date: '2016-05-02', name: 'Tom', address: 'No. 189, Grove St, Los Angeles'},
      {date: '2016-05-04', name: 'Tom', address: 'No. 189, Grove St, Los Angeles'},
      {date: '2016-05-01', name: 'Tom', address: 'No. 189, Grove St, Los Angeles'}
      ]
      </script>

      <template>

      <!-- Button按钮 -->
      <el-row 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>
      </el-row>

      <br>

      <!-- Table表格 -->
      <el-table :data="tableData" border 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>
      </template>

      <style scoped>

      </style>

      此时回到浏览器,我们页面呈现如下效果:

      [!TIP]
      Table 表格组件,属性说明:

      • data: 主要定义 table 组件的数据模型
      • prop: 定义列的数据应该绑定 data 中定义的具体的数据模型
      • label: 定义列的标题
      • width: 定义列的宽度

      image-20260609193433659

      分页条组件

      Pagination: 分页组件,主要提供分页工具条相关功能。其展示效果图下图所示:

      默认情况下,ElementPlus 的组件是英文的,如果希望使用中文语言,可以在 main.js 中做如下配置:

      1
      2
      import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
      app.use(ElementPlus, {locale: zhCn})

      接下来我们通过代码来演示功能。

      首先在官网找到分页组件,我们选择带背景色分页组件,如下图所示:

      然后复制代码到我们的 Element.vue 组件文件的 template 中,在 <template> </template> 拷贝如下代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      <el-pagination
      v-model:current-page="currentPage4"
      v-model:page-size="pageSize4"
      :page-sizes="[100, 200, 300, 400]"
      layout="total, sizes, prev, pager, next, jumper"
      :total="400"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      />

      <script> </script> 中拷贝如下代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      import { ref } from 'vue'

      const currentPage4 = ref(4)
      const pageSize4 = ref(100)

      const handleSizeChange = (val) => {
      console.log(`${val} items per page`)
      }
      const handleCurrentChange = (val) => {
      console.log(`current page: ${val}`)
      }

      目前,整个 Element.vue 的文件内容如下(绿色背景部分代码为本次增加的代码):

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      <script setup>
      import { ref } from 'vue'

      const tableData = [
      {date: '2016-05-03', name: 'Tom', address: 'No. 189, Grove St, Los Angeles'},
      {date: '2016-05-02', name: 'Tom', address: 'No. 189, Grove St, Los Angeles'},
      {date: '2016-05-04', name: 'Tom', address: 'No. 189, Grove St, Los Angeles'},
      {date: '2016-05-01', name: 'Tom', address: 'No. 189, Grove St, Los Angeles'}
      ]

      const currentPage4 = ref(4)
      const pageSize4 = ref(100)

      const handleSizeChange = (val) => {
      console.log(`${val} items per page`)
      }
      const handleCurrentChange = (val) => {
      console.log(`current page: ${val}`)
      }
      </script>

      <template>

      <!-- Button按钮 -->
      <el-row 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>
      </el-row>

      <br>

      <!-- Table表格 -->
      <el-table :data="tableData" border 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>

      <br>

      <el-pagination
      v-model:current-page="currentPage4"
      v-model:page-size="pageSize4"
      :page-sizes="[100, 200, 300, 400]"
      layout="total, sizes, prev, pager, next, jumper"
      :total="400"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      />
      </template>

      <style scoped>

      </style>

      打开浏览器,查看页面效果如下:

      Pagination 分页组件的属性如下:

      对于分页组件我们需要关注的是如下几个重要属性(可以通过查阅官网组件中最下面的组件属性详细说明得到):

      • background: 添加北京颜色,也就是上图蓝色背景色效果。
      • layout: 分页工具条的布局,其具体值包含 sizes, prev, pager, next, jumper, total 这些值
      • total: 数据的总数量

      对于分页组件,除了上述几个属性,还有 2 个非常重要的事件我们需要去学习:

      • size-change : pageSize 改变时会触发
      • current-change :currentPage 改变时会触发

      image-20260609194204444

      对话框组件

      在保留当前页面状态的情况下,告知用户并承载相关操作。

      首先我们需要在 ElementPlus 官方找到 Dialog 组件,如下图所示:

      然后复制如下代码到我们的组件文件 Element.vue<template></template> 模块中:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      <el-button @click="dialogTableVisible = true">
      打开对话框
      </el-button>

      <el-dialog v-model="dialogTableVisible" title="Shipping address">
      <el-table :data="tableData">
      <el-table-column property="date" label="Date" width="150" />
      <el-table-column property="name" label="Name" width="200" />
      <el-table-column property="address" label="Address" />
      </el-table>
      </el-dialog>

      然后复制如下代码到我们的组件文件 Element.vue<script></script> 模块中:

      1
      const dialogTableVisible = ref(false)

      最终,完成的 Element.vue 的代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      <script setup>
      import { ref } from 'vue'

      const tableData = [
      {date: '2016-05-03', name: 'Tom', address: 'No. 189, Grove St, Los Angeles'},
      {date: '2016-05-02', name: 'Tom', address: 'No. 189, Grove St, Los Angeles'},
      {date: '2016-05-04', name: 'Tom', address: 'No. 189, Grove St, Los Angeles'},
      {date: '2016-05-01', name: 'Tom', address: 'No. 189, Grove St, Los Angeles'}
      ]

      const currentPage4 = ref(4)
      const pageSize4 = ref(100)

      const handleSizeChange = (val) => {
      console.log(`${val} items per page`)
      }
      const handleCurrentChange = (val) => {
      console.log(`current page: ${val}`)
      }

      // Dialog对话框
      const dialogTableVisible = ref(false)
      </script>

      <template>

      <!-- Button按钮 -->
      <el-row 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>
      </el-row>

      <br>

      <!-- Table表格 -->
      <el-table :data="tableData" border 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>

      <br>

      <el-pagination
      v-model:current-page="currentPage4"
      v-model:page-size="pageSize4"
      :page-sizes="[100, 200, 300, 400]"
      layout="total, sizes, prev, pager, next, jumper"
      :total="400"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      />

      <br>

      <el-button @click="dialogTableVisible = true">
      打开对话框
      </el-button>

      <el-dialog v-model="dialogTableVisible" title="Shipping address">
      <el-table :data="tableData">
      <el-table-column property="date" label="Date" width="150" />
      <el-table-column property="name" label="Name" width="200" />
      <el-table-column property="address" label="Address" />
      </el-table>
      </el-dialog>

      </template>

      <style scoped>

      </style>

      打开浏览器,最终的页面效果如下:

      [!TIP]
      Dialog 对话框组件使用的关键点,就是控制其显示与隐藏。 通过 v-model 给定的 boolean 值,来控制 Dialog 的显示与隐藏。

      表单组件

      Form 表单:由输入框、选择器、单选框、多选框等控件组成,用以收集、校验、提交数据。

      表单在我们前端的开发中使用的还是比较多的,接下来我们学习这个组件,与之前的流程一样,我们首先需要在 ElementPlus 的官方找到对应的组件示例:如下图所示:

      然后复制如下代码到我们的组件文件 Element.vue<template></template> 模块中:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      <!-- Form 表单 -->
      <el-form :inline="true" :model="formInline" class="demo-form-inline">
      <el-form-item label="Approved by">
      <el-input v-model="formInline.user" placeholder="Approved by" clearable />
      </el-form-item>

      <el-form-item label="Activity zone">
      <el-select v-model="formInline.region" placeholder="Activity zone" clearable>
      <el-option label="Zone one" value="shanghai" />
      <el-option label="Zone two" value="beijing" />
      </el-select>
      </el-form-item>

      <el-form-item label="Activity time">
      <el-date-picker v-model="formInline.date" type="date" placeholder="Pick a date" clearable/>
      </el-form-item>

      <el-form-item>
      <el-button type="primary" @click="onSubmit">Query</el-button>
      </el-form-item>
      </el-form>

      然后复制如下代码到我们的组件文件 Element.vue<script></script> 模块中:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // Form表单
      const formInline = ref({
      user: '',
      region: '',
      date: '',
      })

      const onSubmit = () => {
      console.log('submit!')
      }

      最终,完成的 Element.vue 的代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      <script setup>
      import { ref } from 'vue'

      const tableData = [
      {date: '2016-05-03', name: 'Tom', address: 'No. 189, Grove St, Los Angeles'},
      {date: '2016-05-02', name: 'Tom', address: 'No. 189, Grove St, Los Angeles'},
      {date: '2016-05-04', name: 'Tom', address: 'No. 189, Grove St, Los Angeles'},
      {date: '2016-05-01', name: 'Tom', address: 'No. 189, Grove St, Los Angeles'}
      ]

      const currentPage4 = ref(4)
      const pageSize4 = ref(100)

      const handleSizeChange = (val) => {
      console.log(`${val} items per page`)
      }
      const handleCurrentChange = (val) => {
      console.log(`current page: ${val}`)
      }

      // Dialog对话框
      const dialogTableVisible = ref(false)

      // Form表单
      const formInline = ref({
      user: '',
      region: '',
      date: '',
      })

      const onSubmit = () => {
      console.log('submit!')
      }
      </script>

      <template>

      <!-- Button按钮 -->
      <el-row 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>
      </el-row>

      <br>

      <!-- Table表格 -->
      <el-table :data="tableData" border 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>

      <br>

      <el-pagination
      v-model:current-page="currentPage4"
      v-model:page-size="pageSize4"
      :page-sizes="[100, 200, 300, 400]"
      layout="total, sizes, prev, pager, next, jumper"
      :total="400"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      />

      <br>

      <el-button @click="dialogTableVisible = true">
      打开对话框
      </el-button>

      <el-dialog v-model="dialogTableVisible" title="Shipping address">
      <el-table :data="tableData">
      <el-table-column property="date" label="Date" width="150" />
      <el-table-column property="name" label="Name" width="200" />
      <el-table-column property="address" label="Address" />
      </el-table>
      </el-dialog>

      <br><br>

      <!-- Form 表单 -->
      <el-form :inline="true" :model="formInline" class="demo-form-inline">
      <el-form-item label="Approved by">
      <el-input v-model="formInline.user" placeholder="Approved by" clearable />
      </el-form-item>

      <el-form-item label="Activity zone">
      <el-select v-model="formInline.region" placeholder="Activity zone" clearable>
      <el-option label="Zone one" value="shanghai" />
      <el-option label="Zone two" value="beijing" />
      </el-select>
      </el-form-item>

      <el-form-item label="Activity time">
      <el-date-picker v-model="formInline.date" type="date" placeholder="Pick a date" clearable/>
      </el-form-item>

      <el-form-item>
      <el-button type="primary" @click="onSubmit">Query</el-button>
      </el-form-item>
      </el-form>

      </template>

      <style scoped>

      </style>

      打开浏览器,查看页面效果:

      image-20260609195439478

      image-20260609195418747

      案例

      需求:基于 ElementPlus 组件库制作如下页面,并异步获取数据,完成页面展示。

      1). 准备工作

      由于在案例中,我们需要在 vue 项目中使用 Axios,需要安装 axios,需要在当前项目的目录下执行如下命令:

      1
      npm install axios

      2). 编码实现

      views 目录下,再定义一个文件 EmpList.vue,具体代码实现如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      <script setup>
      import { ref, onMounted } from 'vue'
      import axios from 'axios'

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

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

      const search = async () => {
      # 响应式数据的searchEmp是一个对象需要先获取value才能获取属性
      const url = `https://web-server.itheima.net/emps/list?name=${searchEmp.value.name}&gender=${searchEmp.value.gender}&job=${searchEmp.value.job}`
      const result = await axios.get(url)
      tableData.value = result.data.data
      }

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

      #通过ref定义一个响应式对象
      let tableData = ref([])
      </script>

      <template>
      <div id="center">
      <el-form :inline="true" :model="searchEmp" class="demo-form-inline">
      <el-form-item label="姓名">
      <el-input v-model="searchEmp.name" placeholder="请输入姓名" clearable />
      </el-form-item>
      <el-form-item label="性别">
      <el-select v-model="searchEmp.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="searchEmp.job" placeholder="请选择" clearable>
      <el-option label="班主任" value="1" />
      <el-option label="讲师" value="2" />
      <el-option label="咨询师" value="3" />
      </el-select>
      </el-form-item>
      <el-form-item>
      <el-button type="primary" @click="search">查询</el-button>
      <el-button type="primary" @click="clear">清空</el-button>
      </el-form-item>
      </el-form>
      <br>

      <!-- 表格 -->
      <el-table :data="tableData" border style="width: 100%; ">
      <el-table-column prop="id" label="ID" width="80" align="center" />
      <el-table-column prop="name" label="姓名" width="100" align="center" />
      <el-table-column label="头像" width="120" align="center">
      # 自定义列模版 v-bind 动态为src绑定一个值,可简写成:
      # scope.row拿到一行的数据
      <template #default="scope">
      <img :src="scope.row.image" width="50">
      </template>


      </el-table-column>
      <el-table-column prop="gender" label="性别" width="120" align="center">
      <template #default="scope">
      {{ scope.row.gender == 1 ? '男' : '女' }}
      </template>
      </el-table-column>
      <el-table-column label="职位" width="180" align="center">
      # v-if这些指令是作用在标签上的
      <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>其他</span>
      </template>
      </el-table-column>
      <el-table-column prop="entrydate" label="入职日期" width="180" align="center" />
      <el-table-column prop="updatetime" label="更新时间" align="center" />
      </el-table>
      </div>
      </template>

      <style scoped>
      #center {
      width: 70%;
      margin: auto;
      margin-top: 100px;
      }
      </style>

      App.vue 中引入 EmpList.vue 文件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      <script setup>
      import EmpList from './views/EmpList.vue'
      </script>

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

      <style scoped>

      </style>

      3). 打开浏览器,查看页面效果

      16-前端 Web 实战(Tlias 案例-部门管理)

      在前面的课程中,我们学习了 Vue 工程化的基础内容、TS、ElementPlus,那接下来呢,我们要通过一个案例,加强大家对于 Vue 项目的理解,并掌握 Vue 项目的开发。 这个案例呢,就是我们之前所做的 Tlias 智能学习辅助系统。

      在这个案例中,我们主要完成 部门管理员工管理 的功能开发。 而今天呢,我们先来完成部门管理的功能开发,而在完成部门管理的功能开发之前,先需要完成基础的准备工作。 所以今天的课程安排如下:

      • 前后端分离开发
      • 准备工作
      • 页面布局
      • Vue-Router
      • 部门管理

      前后端分离开发

      在之前的课程中,我们介绍过,现在的企业项目开发有 2 种开发模式:**前后台混合开发 **和 前后台分离开发

      前后台混合开发,顾名思义就是前台后台代码混在一起开发。这种开发模式有如下缺点:

      • 沟通成本高:后台人员发现前端有问题,需要找前端人员修改,前端修改成功,再交给后台人员使用
      • 分工不明确:后台开发人员需要开发后台代码,也需要开发部分前端代码。很难培养专业人才
      • 不便管理:所有的代码都在一个工程中
      • 难以维护:前端代码更新,和后台无关,但是需要整个工程包括后台一起重新打包部署。

      所以我们目前基本都是采用的前后台分离开发方式,如下图所示:

      我们将原先的工程分为前端工程和后端工程这 2 个工程,然后前端工程交给专业的前端人员开发,后端工程交给专业的后端人员开发。

      前端页面需要数据,可以通过发送异步请求,从后台工程获取。但是,我们前后台是分开来开发的,那么前端人员怎么知道后台返回数据的格式呢?后端人员开发,怎么知道前端人员需要的数据格式呢?

      所以针对这个问题,我们前后台统一制定一套规范!我们前后台开发人员都需要遵循这套规范开发,这就是我们的 接口文档。接口文档有离线版和在线版本,接口文档示可以查询今天提供 **资料/接口文档 **里面的资料。

      那么接口文档的内容怎么来的呢?是我们后台开发者根据产品经理提供的产品原型和需求文档所撰写出来的,产品原型示例可以参考今天提供**资料/页面原型 **里面的资料。

      那么基于前后台分离开发的模式下,我们后台开发者开发一个功能的具体流程如何呢?如下图所示:

      1. 需求分析:首先我们需要阅读需求文档,分析需求,理解需求。
      2. 接口定义:查询接口文档中关于需求的接口的定义,包括地址,参数,响应数据类型等等
      3. 前后台并行开发:各自按照接口文档进行开发,实现需求
      4. 测试:前后台开发完了,各自按照接口文档进行测试
      5. 前后段联调测试:前段工程请求后端工程,测试功能

      页面布局

      准备工作

      1. 导入资料中准备的基础工程到 VsCode。 【复制出来放在自己的工作目录下】

      1. 启动前端项目,访问该项目

      打开浏览器,访问 http://localhost:5173

      在提供的基础工程中,最基本的页面布局,已经准备好了。 我们通过页面原型可以看到页面的布局如下:

      其实要实现这个页面布局,我们可以借助于 ElementPlus 中提供的 container 容器布局。

      Container 布局容器,用于布局的容器组件,方便快速搭建页面的基本结构:

      <el-container>:外层容器。 当子元素中包含 <el-header><el-footer> 时,全部子元素会垂直上下排列, 否则会水平左右排列。

      <el-header>:顶栏容器。

      <el-aside>:侧边栏容器。

      <el-main>:主要区域容器。

      <el-footer>:底栏容器。

      [!TIP]
      **提示:**当 <el-container> 子元素中包含 <el-header> <el-footer> 时,全部子元素会垂直上下排列, 否则会水平左右排列。

      左侧菜单布局

      接下来,我们再来完成左侧菜单栏的制作。

      左侧菜单栏的制作,也不需要我们自己实现,其实在 ElementPlus 中已经提供了对应的菜单组件,我们可以直接参考【PS: 其实就是复制过来,参考页面原型和需求,将其改造成我们需要的样子就可以了】。

      参考代码的出处:

      然后就可以参考其提供的源码,复制到我们的侧边栏部分 <el-aside> ... </el-aside>,然后根据我们案例的需要进行改造,改造成我们需要的样子即可。

      最终左侧菜单栏的代码如下 ,大家可以直接导入到 views/layout/index.vue 的侧边栏区域 <el-aside> ... </el-aside>

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      <el-menu>
      <!-- 首页菜单 -->
      <el-menu-item index="/index">
      <el-icon><Promotion /></el-icon> 首页
      </el-menu-item>

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

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

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

      最终,浏览器打开的效果如下:

      到目前为止,views/layout/index.vue 中的整体内容如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      <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-menu>
      <!-- 首页菜单 -->
      <el-menu-item index="/index">
      <el-icon><Promotion /></el-icon> 首页
      </el-menu-item>

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

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

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

      </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>

      目前,我们点击左侧的菜单,右侧主区域展示的内容,还不能做到动态变化。 那应该如何做到动态变化呢 ?

      要解决这个问题,就需要通过 VueRouter 来解决。

      VueRouter

      介绍

      • Vue Router:Vue 的官方路由。 为 Vue 提供富有表现力、可配置的、方便的路由。
      • Vue 中的路由,主要定义的是路径与组件之间的对应关系。

      比如,我们打开一个网站,点击左侧菜单,地址栏的地址发生变化。 地址栏地址一旦发生变化,在主区域显示对应的页面组件。

      VueRouter 主要由以下三个部分组成,如下所示:

      • VueRouter:路由器类,根据路由请求在路由视图中动态渲染选中的组件
      • :请求链接组件,浏览器会解析成
      • :动态视图组件,用来渲染展示与路由路径对应的组件

      基础路由配置

      **1). 在 views/layout/index.vue中,调整代码,具体调整位置如下: **

      • 在左侧菜单栏的 <el-menu> 标签上添加 router 属性,这会让 Element Plus 的 <el-menu> 组件自动根据路由来激活对应的菜单项。
      • 使用 <router-view> 组件来渲染根据路由动态变化的内容。
      • 确保每个 <el-menu-item>index 属性值与你想要导航到的路径相匹配。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      <script setup>
      // 无需额外导入,因为我们只是使用了 Element Plus 和 Vue Router 的基本功能
      </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-menu router>
      <!-- 首页菜单 -->
      <el-menu-item index="/index">
      <el-icon><Promotion /></el-icon> 首页
      </el-menu-item>

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

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

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

      2). 在 router/index.js 中配置请求路径与组件之间的关系。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      import { createRouter, createWebHistory} from 'vue-router';

      import IndexView from '@/views/index/index.vue';
      import ClazzView from '@/views/clazz/index.vue';
      import StuView from '@/views/stu/index.vue';
      import DeptView from '@/views/dept/index.vue';
      import EmpView from '@/views/emp/index.vue';
      import EmpReportView from '@/views/report/emp/index.vue';
      import StuReportView from '@/views/report/stu/index.vue';
      import LogView from '@/views/log/index.vue';
      import LoginView from '@/views/login/index.vue';

      const routes = [
      { path: '/index', component: IndexView },
      { path: '/clazz', component: ClazzView },
      { path: '/stu', component: StuView },
      { path: '/dept', component: DeptView },
      { path: '/emp', component: EmpView },
      { path: '/report/emp', component: EmpReportView },
      { path: '/report/stu', component: StuReportView },
      { path: '/log', component: LogView },
      { path: '/login', component: LoginView },
      ];

      const router = createRouter({
      history: createWebHistory(),
      routes,
      });

      export default router;

      经过这么两步操作之后,我们就可以看到,在页面上,点击左侧菜单,右侧主展示区域,就会显示出对应的页面了。

      那要完成这个功能效果,我们就需要用到 Vue 生态中的路由 Vue-Router

      完善路由配置

      上述我们只是完成了最基本的路由配置。 并经过测试我们发现,如果我们访问 /login 路径,会发现,登录页面是在 layout 页面中嵌套展示的,这肯定是不合适的。

      那接下来,我们就来优化一下路由的配置。最终配置形式如下,在 router/index.js 中做如下配置:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      import { createRouter, createWebHistory } from 'vue-router'

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

      const router = createRouter({
      history: createWebHistory(import.meta.env.BASE_URL),
      routes: [
      {
      path: '/',
      name: '',
      component: LayoutView,
      redirect: '/index', //重定向
      children: [
      {path: 'index', name: 'index', component: IndexView},
      {path: 'clazz', name: 'clazz', component: ClazzView},
      {path: 'stu', name: 'stu', component: StuView},
      {path: 'dept', name: 'dept', component: DeptView},
      {path: 'emp', name: 'emp', component: EmpView},
      {path: 'log', name: 'log', component: LogView},
      {path: 'empReport', name: 'empReport', component: EmpReportView},
      {path: 'stuReport', name: 'stuReport', component: StuReportView},
      ]
      },
      {path: '/login', name: 'login', component: LoginView}
      ]
      })

      export default router

      具体的执行访问流程如下:

      1. 当点击左侧菜单栏的员工管理菜单时,最终地址栏会访问路径 /emp
      2. 此时 VueRouter,会自动的到所配置的路由表(router/index.js)中,查找与该路径对应的组件,并展示在路由展示组件 <router-view> 对应的位置中。

      image-20260610160704043

      部门管理

      部门管理的页面内容,写在 src/views/dept/index.vue 中。

      部门列表

      基本布局

      首先,根据页面原型、需求说明、接口文档,先完成页面的基本布局 。 可以参考 ElementPlus 中的组件,拷贝过来适当做一个改造。

      我们先来完成页面的基本布局。

      部门管理组件 src/views/dept/index.vue 具体的页面布局代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      <script setup>
      import { ref } from 'vue';

      // 示例数据
      const deptList= ref([
      { id: 1, name: '学工部', createTime: '2024-09-01T23:06:29', updateTime: '2024-09-01T23:06:29' },
      { id: 2, name: '教研部', createTime: '2024-09-01T23:06:29', updateTime: '2024-09-01T23:06:29' }
      ]);

      // 编辑部门 - 根据ID查询回显数据
      const handleEdit = (id) => {
      console.log(`Edit item with ID ${id}`);
      // 在这里实现编辑功能
      };

      // 删除部门 - 根据ID删除部门
      const handleDelete = (id) => {
      console.log(`Delete item with ID ${id}`);
      // 在这里实现删除功能
      };
      </script>

      <template>
      <h1>部门管理</h1>

      <!-- 按钮靠页面右侧显示 -->
      <el-button type="primary" @click="handleEdit" style="float: right;"> + 新增部门</el-button> <br><br>

      <el-table :data="deptList" border style="width: 100%;">
      <el-table-column type="index" label="序号" width="100" align="center"/>
      <el-table-column prop="name" label="部门名称" width="300" align="center"/>
      <el-table-column prop="updateTime" label="最后修改时间" width="300" align="center"/>
      <el-table-column fixed="right" label="操作" align="center">
      <template #default="scope">
      <el-button size="small" @click="handleEdit(scope.row.id)">修改</el-button>
      <el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
      </template>
      </el-table-column>
      </el-table>

      </template>

      <style scoped>

      </style>

      表格中每一列展示的属性 prop 都是根据接口文档来的,接口文档返回什么样的数据,我们就安装对应的数据格式进行解析。

      最终页面效果,展示如下:

      加载数据

      根据需求,需要在新增、修改、删除部门之后,加载最新的部门数据。 在打开页面之后,也需要自动加载部门数据。 那接下来,我们就需要基于 axios 发送异步请求,动态获取数据。

      需要在 src/views/dept/index.vue 中增加如下代码,在页面加载完成发送异步请求(https://apifoxmock.com/m1/3128855-1224313-default/depts),动态加载的 Axios。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      <script setup>
      import {ref, onMounted} from 'vue'
      import axios from 'axios'

      //声明列表展示数据
      let deptList= ref([])

      //动态加载数据-查询部门
      const queryAll = async () => {
      const result = await axios.get('https://apifoxmock.com/m1/3128855-1224313-default/depts')
      deptList.value = result.data.data
      }

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

      // 编辑部门 - 根据ID查询回显数据
      const handleEdit = (id) => {
      console.log(`Edit item with ID ${id}`);
      // 在这里实现编辑功能
      };

      // 删除部门 - 根据ID删除部门
      const handleDelete = (id) => {
      console.log(`Delete item with ID ${id}`);
      // 在这里实现删除功能
      };
      </script>

      添加代码后,最终 src/views/dept/index.vue 完整代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      <script setup>
      import {ref, onMounted} from 'vue'
      import axios from 'axios'

      //声明列表展示数据
      let deptList= ref([])

      //动态加载数据-查询部门
      const queryAll = async () => {
      const result = await axios.get('https://apifoxmock.com/m1/3161925-0-default/depts')
      deptList.value = result.data.data
      }

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

      // 编辑部门 - 根据ID查询回显数据
      const handleEdit = (id) => {
      console.log(`Edit item with ID ${id}`);
      // 在这里实现编辑功能
      };

      // 删除部门 - 根据ID删除部门
      const handleDelete = (id) => {
      console.log(`Delete item with ID ${id}`);
      // 在这里实现删除功能
      };
      </script>

      <template>
      <h1>部门管理</h1>

      <!-- 按钮靠页面右侧显示 -->
      <el-button type="primary" @click="handleEdit" style="float: right;"> + 新增部门</el-button> <br><br>

      <el-table :data="deptList" border style="width: 100%;">
      <el-table-column type="index" label="序号" width="100" align="center"/>
      <el-table-column prop="name" label="部门名称" width="300" align="center"/>
      <el-table-column prop="updateTime" label="最后修改时间" width="300" align="center"/>
      <el-table-column fixed="right" label="操作" align="center">
      <template #default="scope">
      <el-button size="small" @click="handleEdit(scope.row.id)">修改</el-button>
      <el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
      </template>
      </el-table-column>
      </el-table>
      </template>

      <style scoped>

      </style>

      页面效果如下,页面一加载,马上就会查询出所有的部门数据:

      我们可以看到数据可以正常的查询出来,并展示在页面中。

      思考:直接在 Vue 组件中,基于 axios 发送异步请求,存在什么问题?

      我们刚才在完成部门列表查询时,是直接基于 axios 发送异步请求,直接将接口的请求地址放在组件文件 .vue 中。 而如果开发一个大型的项目,组件文件可能会很多很多很多,如果前端开发完毕,进行前后端联调测试了,需要修改请求地址,那么此时,就需要找到每一个 .vue 文件,然后挨个修改。 所以上述的代码,虽然实现了动态加载数据的功能。 但是存在以下问题:

      • 请求路径难以维护
      • 数据解析繁琐

      image-20260610164408700

      image-20260610164510865

      理解: 为了避免维护成本,需要将axios自己封装为一个request结构里面加入拦截器统一处理请求,export后,之后请求只需要引入这个模块即可,为了封装与服务端的交互需要统一抽取到api,(导入自定义的request模块封装成请求的函数然后再通过export导出)

      针对于变动的url地址可以配置反向代理到vue.config.js中(可以解决跨域问题)

      image-20260610165425043

      程序优化

      1). 为了解决上述问题,我们在前端项目开发时,通常会定义一个请求处理的工具类 - src/utils/request.js 。 在这个工具类中,对 axios 进行了封装。 具体代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      import axios from 'axios'

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

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

      export default request

      2). 而与服务端进行异步交互的逻辑,通常会按模块,封装在一个单独的 API 中,如:src/api/dept.js

      1
      2
      3
      4
      import request from "@/utils/request"

      //列表查询
      export const queryAllApi = () => request.get('/depts')

      3). 修改 src/views/dept/index.vue 中的代码

      现在就不需要每次直接调用 axios 发送异步请求了,只需要将我们定义的对应模块的 API 导入进来,就可以直接使用了。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      <script setup>
      import {ref, onMounted} from 'vue'
      import {queryAllApi} from '@/api/dept'

      //声明列表展示数据
      let deptList= ref([])

      //动态加载数据-查询部门
      const queryAll = async () => {
      const result = await queryAllApi()
      deptList.value = result.data
      }

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

      // 编辑部门 - 根据ID查询回显数据
      const handleEdit = (id) => {
      console.log(`Edit item with ID ${id}`);
      // 在这里实现编辑功能
      };

      // 删除部门 - 根据ID删除部门
      const handleDelete = (id) => {
      console.log(`Delete item with ID ${id}`);
      // 在这里实现删除功能
      };
      </script>

      做完上面这三步之后,我们打开浏览器发现,并不能访问到接口数据。原因是因为,目前请求路径不对。

      4). 在 vite.config.js 中配置前端请求服务器的信息

      在服务器中配置代理 proxy 的信息,并在配置代理时,执行目标服务器。 以及 url 路径重写的规则。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      import { fileURLToPath, URL } from 'node:url'

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

      // https://vitejs.dev/config/
      export default defineConfig({
      plugins: [
      vue(),
      vueJsx(),
      ],
      resolve: {
      alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
      }
      },
      server: {
      proxy: {
      '/api': {
      target: 'http://localhost:8080',
      secure: false,
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/api/, ''),
      }
      }
      }
      })

      添加内容为,绿色阴影部分的代码。

      然后,我们就可以启动服务器端的程序(将之前开发的服务端程序启动起来测试一下 ),进行测试了(【**注意:测试时, 记得将令牌校验的过滤器及拦截器, 以及记录日志的 AOP 程序 **全部注释】)。

      新增部门

      功能实现

      接下来,我们再来完成新增部门的功能实现。

      新增部门和编辑部门,可以同用一个 Dialog 对话框。而这个对话框中,主要包括两个 ElementPlus 中的组件,分别为:Dialog 对话框组件,Form 表单组件。

      1). 在 src/views/dept/index.vue 中完成页面布局,并编写交互逻辑,完成数据绑定。

      完整代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      <script setup>
      import { ref, onMounted } from 'vue'
      import { ElMessage } from 'element-plus'
      import { queryAllApi, addDeptApi } from '@/api/dept'

      // 声明列表展示数据
      let deptList= ref([])

      // 动态加载数据 - 查询部门
      const queryAll = async () => {
      const result = await queryAllApi()
      deptList.value = result.data
      }

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

      // 编辑部门 - 根据ID查询回显数据
      const handleEdit = (id) => {
      console.log(`Edit item with ID ${id}`);
      // 在这里实现编辑功能
      };

      // 删除部门 - 根据ID删除部门
      const handleDelete = (id) => {
      console.log(`Delete item with ID ${id}`);
      // 在这里实现删除功能
      };


      const formTitle = ref('')
      //新增部门
      const add = () => {
      formTitle.value = '新增部门'
      showDialog.value = true
      deptForm.value = {name: ''}
      }

      // 新增部门对话框的状态
      const showDialog = ref(false)
      // 表单数据
      const deptForm = ref({name: ''})
      // 表单验证规则
      const formRules = ref({
      name: [
      { required: true, message: '请输入部门名称', trigger: 'blur' },
      { min: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
      ]
      })

      // 表单标签宽度
      const formLabelWidth = ref('120px')
      // 表单引用
      const deptFormRef = ref(null)

      // 重置表单
      const resetForm = () => {
      deptFormRef.value.resetFields()
      }

      // 提交表单
      const save = async () => {
      await deptFormRef.value.validate(async valid => {
      if (!valid) return
      // 提交表单
      const result = await addDeptApi(deptForm.value)
      if(result.code){
      ElMessage.success('操作成功')
      // 关闭对话框
      showDialog.value = false
      // 重置表单
      resetForm()
      // 重新加载数据
      queryAll()
      }else {
      ElMessage.error(result.msg)
      }
      })
      }
      </script>

      <template>
      <h1>部门管理</h1>

      <!-- 按钮靠页面右侧显示 -->
      <el-button type="primary" @click="add()" style="float: right;"> + 新增部门</el-button> <br><br>

      <!-- 数据展示表格 -->
      <el-table :data="deptList" border style="width: 100%;">
      <el-table-column type="index" label="序号" width="100" align="center"/>
      <el-table-column prop="name" label="部门名称" width="300" align="center"/>
      <el-table-column prop="updateTime" label="最后修改时间" width="300" align="center"/>
      <el-table-column fixed="right" label="操作" align="center">
      <template #default="scope">
      <el-button size="small" @click="handleEdit(scope.row.id)">修改</el-button>
      <el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
      </template>
      </el-table-column>
      </el-table>

      <!-- 新增部门的对话框 -->
      <el-dialog v-model="showDialog" :title="formTitle" width="30%" @close="resetForm">
      <el-form :model="deptForm" :rules="formRules" ref="deptFormRef">
      <el-form-item label="部门名称" prop="name" label-width="80px">
      <el-input v-model="deptForm.name" autocomplete="off"></el-input>
      </el-form-item>
      </el-form>

      <template #footer>
      <span class="dialog-footer">
      <el-button @click="showDialog = false">取消</el-button>
      <el-button type="primary" @click="save">确定</el-button>
      </span>
      </template>
      </el-dialog>

      </template>

      <style scoped>

      </style>

      2). 在 src/api/dept.js 中增加如下代码

      1
      2
      //添加部门
      export const addDeptApi = (data) => request.post('/depts', data)

      目前 src/api/dept.js 文件中完整代码如下:

      1
      2
      3
      4
      5
      6
      7
      import request from '@/utils/request'

      //查询全部部门
      export const queryAllApi = () => request.get('/depts')

      //添加部门
      export const addDeptApi = (data) => request.post('/depts', data)

      打开浏览器进行测试,效果如下:

      点击保存之后,就会将数据保存到数据库,并重新查询出最新的数据,展示在页面中。

      表单校验

      Form 组件允许你验证用户的输入是否符合规范,来帮助你找到和纠正错误。Form 组件提供了表单验证的功能,只需为 rules 属性传入约定的验证规则,并将 form-Itemprop 属性设置为需要验证的特殊键值即可。

      操作步骤:

      • 通过 ref 属性注册元素的引用。
      • 定义表单校验规则,通过 rules 属性与表单绑定,并通过 prop 属性绑定对应的表单项。
      • 表单提交时,校验表单,校验通过,则允许提交表单。

      修改部门

      对于修改操作,通常会分为两步进行:

      1. 查询回显
      2. 保存修改

      交互逻辑:

      1. 点击 编辑 按钮,根据 ID 进行查询,弹出对话框,完成页面回显展示。(查询回显)
      2. 点击 确定 按钮,保存修改后的数据,完成数据更新操作。(保存修改)

      查询回显

      1). 在 src/api/dept.js 中定义根据 id 查询的请求

      1
      2
      //根据ID查询
      export const queryInfoApi = (id) => request.get(`/depts/${id}`)

      2). 在 src/views/dept/index.vue 中添加根据 ID 查询回显的逻辑

      为修改按钮绑定事件 <template></template>:

      1
      <el-button size="small" @click="handleEdit(scope.row.id)">修改</el-button>

      <script> </script> 添加 JS 逻辑:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // 编辑部门 - 根据ID查询回显数据
      const handleEdit = async (id) => {
      console.log(`Edit item with ID ${id}`);
      formTitle.value = '修改部门'
      showDialog.value = true
      deptForm.value = {name: ''}

      const result = await queryInfoApi(id)
      if(result.code){
      deptForm.value = result.data
      }
      };

      到目前为止,完整的 src/views/dept/index.vue 代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      <script setup>
      import { ref, onMounted } from 'vue'
      import { ElMessage } from 'element-plus'
      import { queryAllApi, addDeptApi, queryInfoApi } from '@/api/dept'

      // 声明列表展示数据
      let deptList= ref([])

      // 动态加载数据 - 查询部门
      const queryAll = async () => {
      const result = await queryAllApi()
      deptList.value = result.data
      }

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

      // 编辑部门 - 根据ID查询回显数据
      const handleEdit = async (id) => {
      console.log(`Edit item with ID ${id}`);
      formTitle.value = '修改部门'
      showDialog.value = true
      deptForm.value = {name: ''}

      const result = await queryInfoApi(id)
      if(result.code){
      deptForm.value = result.data
      }
      };

      const formTitle = ref('')
      //新增部门
      const add = () => {
      formTitle.value = '新增部门'
      showDialog.value = true
      deptForm.value = {name: ''}
      }

      // 删除部门 - 根据ID删除部门
      const handleDelete = (id) => {
      console.log(`Delete item with ID ${id}`);
      // 在这里实现删除功能
      };

      // 新增部门对话框的状态
      const showDialog = ref(false)
      // 表单数据
      const deptForm = ref({name: ''})
      // 表单验证规则
      const formRules = ref({
      name: [
      { required: true, message: '请输入部门名称', trigger: 'blur' },
      { min: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
      ]
      })

      // 表单引用
      const deptFormRef = ref(null)

      // 重置表单
      const resetForm = () => {
      deptFormRef.value.resetFields()
      }

      // 提交表单
      const save = async () => {
      await deptFormRef.value.validate(async valid => {
      if (!valid) return
      // 提交表单
      const result = await addDeptApi(deptForm.value)
      if(result.code){
      ElMessage.success('操作成功')
      // 关闭对话框
      showDialog.value = false
      // 重置表单
      resetForm()
      // 重新加载数据
      queryAll()
      }else {
      ElMessage.error(result.msg)
      }
      })
      }
      </script>

      <template>
      <h1>部门管理</h1>

      <!-- 按钮靠页面右侧显示 -->
      <el-button type="primary" @click="add()" style="float: right;"> + 新增部门</el-button> <br><br>

      <!-- 数据展示表格 -->
      <el-table :data="deptList" border style="width: 100%;">
      <el-table-column type="index" label="序号" width="100" align="center"/>
      <el-table-column prop="name" label="部门名称" width="300" align="center"/>
      <el-table-column prop="updateTime" label="最后修改时间" width="300" align="center"/>
      <el-table-column fixed="right" label="操作" align="center">
      <template #default="scope">
      <el-button size="small" @click="handleEdit(scope.row.id)">修改</el-button>
      <el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
      </template>
      </el-table-column>
      </el-table>

      <!-- 新增部门的对话框 -->
      <el-dialog v-model="showDialog" :title="formTitle" width="30%" @close="resetForm">
      <el-form :model="deptForm" :rules="formRules" ref="deptFormRef">
      <el-form-item label="部门名称" prop="name" label-width="80px">
      <el-input v-model="deptForm.name" autocomplete="off"></el-input>
      </el-form-item>
      </el-form>

      <template #footer>
      <span class="dialog-footer">
      <el-button @click="showDialog = false">取消</el-button>
      <el-button type="primary" @click="save">确定</el-button>
      </span>
      </template>
      </el-dialog>

      </template>

      <style scoped>

      </style>

      打开浏览器测试一下,根据 ID 查询部门页面回显:

      保存修改

      由于 新增部门 和 修改部门使用的是同一个 Dialog 对话框,当前点击 “确定” 按钮的时候,有可能执行的是新增操作,也有可能是修改操作。

      那应该如何辨别到底是新增,还是修改操作呢 ?

      其实,我们只需要根据 deptForm 对象的 id 属性值,来判断即可。 如果没有 id,则是新增操作 ;如果有 id,则是修改操作。

      所以,保存修改功能实现如下:

      1). 在 src/api/dept.js 中增加如下修改部门的请求

      1
      2
      //修改部门
      export const updateDeptApi = (data) => request.put('/depts', data)

      2). 在 src/views/dept/index.vue 中引入上述函数,并完善(修改) save 函数的逻辑

      1
      import { queryAllApi, addDeptApi, queryInfoApi, updateDeptApi } from '@/api/dept'
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      // 提交表单
      const save = async () => {
      await deptFormRef.value.validate(async valid => {
      if (!valid) return
      // 提交表单
      let result = null;
      if(deptForm.value.id){
      result = await updateDeptApi(deptForm.value) // 修改
      }else {
      result = await addDeptApi(deptForm.value) // 新增
      }
      if(result.code){
      ElMessage.success('操作成功')
      // 关闭对话框
      showDialog.value = false
      // 重置表单
      resetForm()
      // 重新加载数据
      queryAll()
      }else {
      ElMessage.error(result.msg)
      }
      })
      }

      最终完整代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      <script setup>
      import { ref, onMounted } from 'vue'
      import { ElMessage } from 'element-plus'
      import { queryAllApi, addDeptApi, queryInfoApi, updateDeptApi } from '@/api/dept'

      // 声明列表展示数据
      let deptList = ref([])

      // 动态加载数据 - 查询部门
      const queryAll = async () => {
      const result = await queryAllApi()
      deptList .value = result.data
      }

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

      const formTitle = ref('')
      //新增部门
      const add = () => {
      formTitle.value = '新增部门'
      showDialog.value = true
      deptForm.value = {name: ''}
      }

      // 编辑部门 - 根据ID查询回显数据
      const handleEdit = async (id) => {
      console.log(`Edit item with ID ${id}`);
      formTitle.value = '修改部门'
      showDialog.value = true
      deptForm.value = {name: ''}

      const result = await queryInfoApi(id)
      if(result.code){
      deptForm.value = result.data
      }
      };

      // 删除部门 - 根据ID删除部门
      const handleDelete = (id) => {
      console.log(`Delete item with ID ${id}`);
      // 在这里实现删除功能
      };

      // 新增部门对话框的状态
      const showDialog = ref(false)
      // 表单数据
      const deptForm = ref({name: ''})
      // 表单验证规则
      const formRules = ref({
      name: [
      { required: true, message: '请输入部门名称', trigger: 'blur' },
      { min: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
      ]
      })

      // 表单引用
      const deptFormRef = ref(null)

      // 重置表单
      const resetForm = () => {
      deptFormRef.value.resetFields()
      }

      // 提交表单
      const save = async () => {
      await deptFormRef.value.validate(async valid => {
      if (!valid) return
      // 提交表单
      let result = null;
      if(deptForm.value.id){
      result = await updateDeptApi(deptForm.value) // 修改
      }else {
      result = await addDeptApi(deptForm.value) // 新增
      }
      if(result.code){
      ElMessage.success('操作成功')
      // 关闭对话框
      showDialog.value = false
      // 重置表单
      resetForm()
      // 重新加载数据
      queryAll()
      }else {
      ElMessage.error(result.msg)
      }
      })
      }
      </script>

      <template>
      <h1>部门管理</h1>

      <!-- 按钮靠页面右侧显示 -->
      <el-button type="primary" @click="add()" style="float: right;"> + 新增部门</el-button> <br><br>

      <!-- 数据展示表格 -->
      <el-table :data="deptList" border style="width: 100%;">
      <el-table-column type="index" label="序号" width="100" align="center"/>
      <el-table-column prop="name" label="部门名称" width="300" align="center"/>
      <el-table-column prop="updateTime" label="最后修改时间" width="300" align="center"/>
      <el-table-column fixed="right" label="操作" align="center">
      <template #default="scope">
      <el-button size="small" @click="handleEdit(scope.row.id)">修改</el-button>
      <el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
      </template>
      </el-table-column>
      </el-table>

      <!-- 新增部门的对话框 -->
      <el-dialog v-model="showDialog" :title="formTitle" width="30%" @close="resetForm">
      <el-form :model="deptForm" :rules="formRules" ref="deptFormRef">
      <el-form-item label="部门名称" prop="name" label-width="80px">
      <el-input v-model="deptForm.name" autocomplete="off"></el-input>
      </el-form-item>
      </el-form>

      <template #footer>
      <span class="dialog-footer">
      <el-button @click="showDialog = false">取消</el-button>
      <el-button type="primary" @click="save">确定</el-button>
      </span>
      </template>
      </el-dialog>

      </template>

      <style scoped>

      </style>

      打开浏览器,测试修改员工功能:

      删除部门

      • 点击删除按钮,需要删除当前这条数据,删除完成之后,刷新页面,展示出最新的数据。
      • 由于删除是一个比较危险的操作,为避免误操作,通常会在点击删除之后,弹出确认框进行确认。
      • Element 组件:ElMessageBox 消息弹出框组件

      具体操作步骤:

      1). 在 src/api/dept.js 中增加如下删除部门的请求

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

      2). 在 src/views/dept/index.vue 中为 删除 按钮绑定事件

      1
      <el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>

      3). 在 src/views/dept/index.vue 编写根据 ID 删除数据的函数 。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      // 删除部门 - 根据ID删除部门
      const handleDelete = (id) => {
      console.log(`Delete item with ID ${id}`);
      //删除部门时, 需要弹出一个确认框, 如果是确认, 则删除部门
      ElMessageBox.confirm('此操作将永久删除该部门, 是否继续?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
      }).then(async () => {
      // 删除部门
      const result = await deleteDeptApi(id)
      if(result.code){
      ElMessage.success('删除成功')
      queryAll()
      }
      })
      };

      注意上述,用到了 ElementPlus 中的 ElMessageBox 组件,需要 import 导入进来。代码如下:

      1
      import { ElMessage, ElMessageBox } from 'element-plus'

      到目前为止,完整的 src/views/dept/index.vue 代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      <script setup>
      import { ref, onMounted } from 'vue'
      import { ElMessage, ElMessageBox } from 'element-plus'
      import { queryAllApi, addDeptApi, queryInfoApi, updateDeptApi, deleteDeptApi } from '@/api/dept'

      // 声明列表展示数据
      let deptList= ref([])

      // 动态加载数据 - 查询部门
      const queryAll = async () => {
      const result = await queryAllApi()
      deptList.value = result.data
      }

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

      const formTitle = ref('')
      //新增部门
      const add = () => {
      formTitle.value = '新增部门'
      showDialog.value = true
      deptForm.value = {name: ''}
      }

      // 编辑部门 - 根据ID查询回显数据
      const handleEdit = async (id) => {
      console.log(`Edit item with ID ${id}`);
      formTitle.value = '修改部门'
      showDialog.value = true
      deptForm.value = {name: ''}

      const result = await queryInfoApi(id)
      if(result.code){
      deptForm.value = result.data
      }
      };

      // 删除部门 - 根据ID删除部门
      const handleDelete = (id) => {
      console.log(`Delete item with ID ${id}`);
      //删除部门时, 需要弹出一个确认框, 如果是确认, 则删除部门
      ElMessageBox.confirm('此操作将永久删除该部门, 是否继续?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
      }).then(async () => {
      // 删除部门
      const result = await deleteDeptApi(id)
      if(result.code){
      ElMessage.success('删除成功')
      queryAll()
      }
      })
      };

      // 新增部门对话框的状态
      const showDialog = ref(false)
      // 表单数据
      const deptForm = ref({name: ''})
      // 表单验证规则
      const formRules = ref({
      name: [
      { required: true, message: '请输入部门名称', trigger: 'blur' },
      { min: 2, max: 10, message: '长度在 2 到 10 个字符', trigger: 'blur' }
      ]
      })

      // 表单引用
      const deptFormRef = ref(null)

      // 重置表单
      const resetForm = () => {
      deptFormRef.value.resetFields()
      }

      // 提交表单
      const save = async () => {
      await deptFormRef.value.validate(async valid => {
      if (!valid) return
      // 提交表单
      let result = null;
      if(deptForm.value.id){
      result = await updateDeptApi(deptForm.value) // 修改
      }else {
      result = await addDeptApi(deptForm.value) // 新增
      }
      if(result.code){
      ElMessage.success('操作成功')
      // 关闭对话框
      showDialog.value = false
      // 重置表单
      resetForm()
      // 重新加载数据
      queryAll()
      }else {
      ElMessage.error(result.msg)
      }
      })
      }
      </script>

      <template>
      <h1>部门管理</h1>

      <!-- 按钮靠页面右侧显示 -->
      <el-button type="primary" @click="add()" style="float: right;"> + 新增部门</el-button> <br><br>

      <!-- 数据展示表格 -->
      <el-table :data="deptList" border style="width: 100%;">
      <el-table-column type="index" label="序号" width="100" align="center"/>
      <el-table-column prop="name" label="部门名称" width="300" align="center"/>
      <el-table-column prop="updateTime" label="最后修改时间" width="300" align="center"/>
      <el-table-column fixed="right" label="操作" align="center">
      <template #default="scope">
      <el-button size="small" @click="handleEdit(scope.row.id)">修改</el-button>
      <el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
      </template>
      </el-table-column>
      </el-table>

      <!-- 新增部门的对话框 -->
      <el-dialog v-model="showDialog" :title="formTitle" width="30%" @close="resetForm">
      <el-form :model="deptForm" :rules="formRules" ref="deptFormRef">
      <el-form-item label="部门名称" prop="name" label-width="80px">
      <el-input v-model="deptForm.name" autocomplete="off"></el-input>
      </el-form-item>
      </el-form>

      <template #footer>
      <span class="dialog-footer">
      <el-button @click="showDialog = false">取消</el-button>
      <el-button type="primary" @click="save">确定</el-button>
      </span>
      </template>
      </el-dialog>

      </template>

      <style scoped>

      </style>

      打开浏览器,测试删除员工功能:

      17-前端 Web 实战(Tlias 案例-员工管理)

      在昨天的课程中,我们完成了部门管理的功能开发。 那今天呢,我们要来开发的是员工管理的功能。 我们来看一下员工管理的页面原型:

      包括如下几个部分:

      • 条件分页查询
      • 新增员工
      • 修改员工
      • 删除员工

      除了员工管理的功能以外,还涉及到登录、退出等功能。

      条件分页查询

      介绍

      在页面原型中,我们可以看到在查询员工信息列表时,既需要根据条件动态查询,还需要对查询的结果进行分页处理。

      那接下来,我们在制作这个页面时,将先完成基本的页面布局,再完成数据的动态加载。 而页面布局,又分为 4 个部分,分别为:

      • 搜索栏
      • 按钮
      • 数据展示表格
      • 分页条

      页面布局

      搜索表单

      1). 在 views/emp/index.vue 中的 <script setup></script> 中增加如下 js 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      import { ref} from 'vue'

      const searchEmp = ref({
      name: '',
      gender: '',
      date: [],
      begin: '',
      end: ''
      })

      const search = () => {
      // 处理查询逻辑
      console.log('Search:', searchEmp.value)
      }

      const clear = () => {
      // 清空表单
      searchEmp.value = {
      name: '',
      gender: '',
      date: []
      }
      search()
      }

      2). 在 views/emp/index.vue 中的 <template></template> 中增加如下 js 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      <h1>员工管理</h1> <br>
      {{ searchEmp }}
      <el-form :inline="true" :model="searchEmp">
      <el-form-item label="姓名">
      <el-input v-model="searchEmp.name" placeholder="请输入员工姓名"></el-input>
      </el-form-item>

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

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

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

      注意:在 element-plus 中,提供的日期选择组件,最终提交的值的格式,如果为 2024-02-01 这种格式,需要设置 value-format 属性为 YYYY-MM-DD

      我们在测试的时候,为了能够看到搜索表单采集到的数据,可以直接在通过插值表达式 在表单上方,将搜索表单项的值直接输出出来。

      打开浏览器,我们可以看到如下页面效果:

      通过测试,我们可以看到,输入的入职日期的搜索条件,用的是 element-plus 的日期范围组件,开始时间和结束时间,两个值,是封装到了一个数组中。

      而通过接口接口文档,我们可以可以看到最终在查询员工列表数据时,搜索条件中,入职日期的开始时间 和 结束时间,是两个值,一个是 begin 开始时间,一个是 end 结束时间。

      那么此时,我们就需要将 searchEmp 对象中的 date 这个数组的值,解析出来,复制给 beginend。而且,一旦 date 的值发生变化,我们就需要重新计算 beginend 这两个时间,把数组中的第一个元素,赋值给 begin;第二个元素,赋值给 end。

      而要实现这个效果,我们就需要用到 Vue3 中提供的 watch 侦听。

      watch 侦听

      • 作用:侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。

      • 用法:

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

      • 侦听对象的单个属性

      • 侦听对象的全部属性

      第三个可选参数,常见两个选项:

      介绍完了 watch 侦听的基本语法之后,我们再来完成搜索栏中员工入职日期的开始时间和结束时间的处理。 我们要做到,在输入的时间发生变化时,自动为 searchEmp 中的 begin,end 两个属性赋值。

      具体代码如下,在 views/emp/index.vue 文件中的 <script></script> 中增加 watch 监听:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      import { ref, watch } from 'vue'

      const searchEmp = ref({
      name: '',
      gender: '',
      date: [],
      begin: '',
      end: ''
      })

      //侦听searchEmp中的date属性
      watch(
      () => searchEmp.value.date,
      (newValue, oldValue) => {
      if(newValue.length == 2){
      searchEmp.value.begin = newValue[0]
      searchEmp.value.end = newValue[1]
      }else {
      searchEmp.value.begin = ''
      searchEmp.value.end = ''
      }
      }
      )

      const search= () => {
      // 处理查询逻辑
      console.log('Search:', searchEmp.value)
      }

      const clear = () => {
      // 清空表单
      searchEmp.value = {
      name: '',
      gender: '',
      date: [],
      begin: '',
      end: ''
      }
      search()
      }

      image-20260610200654123

      表格展示

      接下来,我们再来制作这个表格。通过页面原型,我们可以看到表格的展示内容及展示形式:

      调整后如下所示(绿色背景部分为新添加部分):

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      <script setup>
      import { ref, watch } from 'vue'

      const searchEmp = ref({
      name: '',
      gender: '',
      date: [],
      begin: '',
      end: ''
      })

      //侦听searchEmp中的date属性
      watch(
      () => searchEmp.value.date,
      (newValue, oldValue) => {
      if(newValue.length == 2){
      searchEmp.value.begin = newValue[0]
      searchEmp.value.end = newValue[1]
      }else {
      searchEmp.value.begin = ''
      searchEmp.value.end = ''
      }
      }
      )

      const search= () => {
      // 处理查询逻辑
      console.log('Search:', searchEmp.value)
      }

      const clear = () => {
      // 清空表单
      searchEmp.value = {
      name: '',
      gender: '',
      date: [],
      begin: '',
      end: ''
      }
      search()
      }

      // 示例数据
      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 total = ref(0)

      // 分页处理
      const handleSizeChange = (val) => {
      search()
      }
      const handleCurrentChange = (val) => {
      search()
      }


      </script>

      <template>
      <h1>员工管理</h1> <br>
      {{ searchEmp }}
      <el-form :inline="true" :model="searchEmp">
      <el-form-item label="姓名">
      <el-input v-model="searchEmp.name" placeholder="请输入员工姓名"></el-input>
      </el-form-item>

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

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

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

      <el-button type="primary" @click=""> + 新增员工</el-button>
      <el-button type="danger" @click=""> - 批量删除</el-button>
      <br><br>

      <!-- 表格 -->
      <el-table :data="empList" border style="width: 100%">
      <el-table-column type="selection" width="55" align="center"></el-table-column>
      <el-table-column prop="name" label="姓名" width="120" align="center"></el-table-column>
      <el-table-column label="性别" width="170" align="center">
      <template #default="scope" >
      {{ scope.row.gender == 1 ? '男' : '女' }}
      </template>
      </el-table-column>
      <el-table-column label="头像" width="170" align="center">
      <template #default="scope" >
      <img :src="scope.row.image" alt="Avatar" class="avatar" />
      </template>
      </el-table-column>
      <el-table-column prop="deptName" label="部门名称" width="170" align="center"></el-table-column>
      <el-table-column 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>
      <el-table-column prop="updateTime" label="最后操作时间" width="210" align="center"></el-table-column>
      <el-table-column label="操作" fixed="right" align="center">
      <template #default="scope">
      <el-button size="small" type="primary" @click="">编辑</el-button>
      <el-button size="small" type="danger" @click="">删除</el-button>
      </template>
      </el-table-column>
      </el-table>

      <br>

      <!-- 分页 -->
      <el-pagination
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :page-sizes="[10, 20, 30, 40]"
      layout="total, sizes, prev, pager, next, jumper"
      :total="total"
      >
      </el-pagination>
      </template>

      <style scoped>
      .avatar {
      height: 40px;
      }
      </style>

      代码编写完毕后,我们打开浏览器会看到如下效果:

      我们可以看到,图片、职位字段都正常展示出来了。

      页面交互

      基本的页面布局完成之后,接下来,我们再来完成页面的交互效果,具体效果有如下三项:

      • 页面加载完毕后,查询员工信息列表。
      • 点击查询按钮,查询员工信息列表。
      • 当页码、每页展示记录数发生变化时,查询员工信息列表。

      那么要想实现上述的效果,我们就需要在 Vue 的钩子函数 onMounted 中,调用查询方法 search,然后在 search 方法中发起异步请求,请求服务端。

      当页码、每页展示的记录数发生变化时,会触发 handleSizeChange、handleCurrentChange 方法,然后在这两个方法中再调用 search 方法,发起异步请求,请求服务端即可。

      具体代码实现如下:

      **1). 在 src/api 目录下再定义一个 **emp.js

      我们可以一次性将增删改查相关的 api 操作的函数都定义好。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      import request from "@/utils/request";

      //查询员工列表数据
      export const queryPageApi = (name,gender,begin,end,page,pageSize) =>
      request.get(`/emps?name=${name}&gender=${gender}&begin=${begin}&end=${end}&page=${page}&pageSize=${pageSize}`)

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

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

      //修改
      export const updateApi = (emp) => request.put('/emps', emp);

      //删除
      export const deleteApi = (ids) => request.delete(`/emps?ids=${ids}`);

      2). 在 src/views/emp/index.vue 文件中完成页面交互实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      <script setup>
      import { ref, watch, onMounted } from 'vue'
      import { queryPageApi } from '@/api/emp'

      const searchEmp = ref({
      name: '',
      gender: '',
      date: [],
      begin: '',
      end: ''
      })

      //侦听searchEmp中的date属性
      watch(
      () => searchEmp.value.date,
      (newValue, oldValue) => {
      if(newValue.length == 2){
      searchEmp.value.begin = newValue[0]
      searchEmp.value.end = newValue[1]
      }else {
      searchEmp.value.begin = ''
      searchEmp.value.end = ''
      }
      }
      )

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

      //查询员工
      const search= async () => {
      console.log('Search:', searchEmp.value)
      const result = await queryPageApi(searchEmp.value.name, searchEmp.value.gender, searchEmp.value.begin, searchEmp.value.end, currentPage.value, pageSize.value);
      if(result.code){
      empList.value = result.data.rows
      total.value = result.data.total
      }
      }

      const clear = () => {
      // 清空表单
      searchEmp.value = {
      name: '',
      gender: '',
      date: [],
      begin: '',
      end: ''
      }
      search()
      }

      // 示例数据
      const empList = ref([])

      // 分页配置
      const currentPage = ref(1)
      const pageSize = ref(10)
      const total = ref(0)

      // 分页处理
      const handleSizeChange = (val) => {
      search()
      }
      const handleCurrentChange = (val) => {
      search()
      }

      </script>

      <template>
      <h1>员工管理</h1> <br>
      {{ searchEmp }}
      <el-form :inline="true" :model="searchEmp">
      <el-form-item label="姓名">
      <el-input v-model="searchEmp.name" placeholder="请输入员工姓名"></el-input>
      </el-form-item>

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

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

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

      <el-button type="primary" @click=""> + 新增员工</el-button>
      <el-button type="danger" @click=""> - 批量删除</el-button>
      <br><br>

      <!-- 表格 -->
      <el-table :data="empList" border style="width: 100%">
      <el-table-column type="selection" width="55" align="center"></el-table-column>
      <el-table-column prop="name" label="姓名" width="120" align="center"></el-table-column>
      <el-table-column label="性别" width="80" align="center">
      <template #default="scope" >
      {{ scope.row.gender == 1 ? '男' : '女' }}
      </template>
      </el-table-column>
      <el-table-column label="头像" width="170" align="center">
      <template #default="scope" >
      <img :src="scope.row.image" class="avatar" />
      </template>
      </el-table-column>
      <el-table-column prop="deptName" label="部门名称" width="170" align="center"></el-table-column>
      <el-table-column 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>
      <el-table-column prop="updateTime" label="最后操作时间" width="210" align="center"></el-table-column>
      <el-table-column label="操作" fixed="right" align="center">
      <template #default="scope">
      <el-button size="small" type="primary" @click="">编辑</el-button>
      <el-button size="small" type="danger" @click="">删除</el-button>
      </template>
      </el-table-column>
      </el-table>

      <br>

      <!-- 分页 -->
      <el-pagination
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :page-sizes="[5, 10, 20, 30, 50, 75, 100]"
      :background="background"
      layout="total, sizes, prev, pager, next, jumper"
      :total="total"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      />
      </el-pagination>
      </template>

      <style scoped>
      .avatar {
      height: 40px;
      }
      </style>

      代码编写完成之后,我们可以打开浏览器测试一下:

      新增员工

      页面布局

      需求

      从页面原型中,我们可以看到,新增员工信息的表单包含两个部分,分别为:

      • 员工的基本信息
      • 员工的工作经历信息

      基本布局实现

      接下来,我们也可以基于 AI 来帮我们完成页面的整体布局。

      src/views/emp/index.vue 中增加如下代码(绿色背景部分):

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
      282
      283
      284
      285
      286
      287
      288
      289
      290
      291
      292
      293
      294
      295
      296
      297
      298
      299
      300
      301
      302
      303
      304
      305
      306
      307
      308
      309
      310
      311
      312
      313
      314
      315
      316
      317
      318
      319
      320
      321
      322
      323
      324
      325
      326
      327
      328
      329
      330
      331
      332
      333
      334
      335
      336
      337
      338
      339
      340
      341
      342
      343
      344
      345
      346
      347
      348
      349
      350
      351
      352
      353
      354
      355
      356
      357
      358
      359
      360
      361
      362
      363
      364
      365
      366
      367
      368
      369
      370
      371
      372
      373
      374
      375
      <script setup>
      import { ref, watch, onMounted } from 'vue'
      import { ElMessage } from 'element-plus'
      import { queryPageApi } from '@/api/emp'

      const searchEmp = ref({
      name: '',
      gender: '',
      date: [],
      begin: '',
      end: ''
      })

      //侦听searchEmp中的date属性
      watch(
      () => searchEmp.value.date,
      (newValue, oldValue) => {
      if(newValue.length == 2){
      searchEmp.value.begin = newValue[0]
      searchEmp.value.end = newValue[1]
      }else {
      searchEmp.value.begin = ''
      searchEmp.value.end = ''
      }
      }
      )

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

      //查询员工
      const search= async () => {
      console.log('Search:', searchEmp.value)
      const result = await queryPageApi(searchEmp.value.name, searchEmp.value.gender, searchEmp.value.begin, searchEmp.value.end, currentPage.value, pageSize.value);
      if(result.code){
      empList.value = result.data.rows
      total.value = result.data.total
      }
      }

      const clear = () => {
      // 清空表单
      searchEmp.value = {
      name: '',
      gender: '',
      date: [],
      begin: '',
      end: ''
      }
      search()
      }

      // 示例数据
      const empList = ref([])

      // 分页配置
      const currentPage = ref(1)
      const pageSize = ref(10)
      const total = ref(0)


      // 分页处理
      const handleSizeChange = (val) => {
      search();
      }
      const handleCurrentChange = (val) => {
      search();
      }


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


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

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

      //文件上传
      // 图片上传成功后触发
      const handleAvatarSuccess = (response,uploadFile) => {
      employee.value.image = response.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>

      <template>
      <h1>员工管理</h1> <br>
      <el-form :inline="true" :model="searchEmp">
      <el-form-item label="姓名">
      <el-input v-model="searchEmp.name" placeholder="请输入员工姓名"></el-input>
      </el-form-item>

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

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

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

      <el-button type="primary" @click="addEmp"> + 新增员工</el-button>
      <el-button type="danger" @click=""> - 批量删除</el-button>
      <br><br>

      <!-- 表格 -->
      <el-table :data="empList" border style="width: 100%">
      <el-table-column type="selection" width="55" align="center"></el-table-column>
      <el-table-column prop="name" label="姓名" width="120" align="center"></el-table-column>
      <el-table-column label="性别" width="80" align="center">
      <template #default="scope" >
      {{ scope.row.gender == 1 ? '男' : '女' }}
      </template>
      </el-table-column>
      <el-table-column label="头像" width="170" align="center">
      <template #default="scope" >
      <img :src="scope.row.image" alt="Avatar" class="avatar" />
      </template>
      </el-table-column>
      <el-table-column prop="deptName" label="部门名称" width="170" align="center"></el-table-column>
      <el-table-column 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>
      <el-table-column prop="updateTime" label="最后操作时间" width="210" align="center"></el-table-column>
      <el-table-column label="操作" fixed="right" align="center">
      <template #default="scope">
      <el-button size="small" type="primary" @click="">编辑</el-button>
      <el-button size="small" type="danger" @click="">删除</el-button>
      </template>
      </el-table-column>
      </el-table>

      <br>

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

      <!-- 新增/修改员工的对话框 -->
      <el-dialog v-model="dialogVisible" :title="dialogTitle">
      <el-form ref="employeeFormRef" :model="employee" label-width="80px">
      <!-- 基本信息 -->
      <!-- 第一行 -->
      <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: 1px dashed var(--el-border-color);
      }
      </style>

      代码编写完毕后,打开浏览器看到最终效果如下:

      这样最基本的布局就有了,接下来,我们需要对其进行进一步的优化。

      页面布局优化

      表单中的数据动态展示 。比如:所属部门、职位、性别,这些源数据我们可以定义在 js 代码中,在页面中基于 v-for 指令,遍历展示出来,便于项目的统一维护处理。

      优化后的页面内容如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
      282
      283
      284
      285
      286
      287
      288
      289
      290
      291
      292
      293
      294
      295
      296
      297
      298
      299
      300
      301
      302
      303
      304
      305
      306
      307
      308
      309
      310
      311
      312
      313
      314
      315
      316
      317
      318
      319
      320
      321
      322
      323
      324
      325
      326
      327
      328
      329
      330
      331
      332
      333
      334
      335
      336
      337
      338
      339
      340
      341
      342
      343
      344
      345
      346
      347
      348
      349
      350
      351
      352
      353
      354
      355
      356
      357
      358
      359
      360
      361
      362
      363
      364
      365
      366
      367
      368
      369
      370
      371
      372
      373
      374
      375
      376
      377
      378
      379
      380
      381
      382
      <script setup>
      import { ref, watch, onMounted } from 'vue'
      import { ElMessage } from 'element-plus'
      import { queryPageApi } from '@/api/emp'
      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 deptList = ref([])

      const searchEmp = ref({
      name: '',
      gender: '',
      date: [],
      begin: '',
      end: ''
      })

      //侦听searchEmp中的date属性
      watch(
      () => searchEmp.value.date,
      (newValue, oldValue) => {
      if(newValue.length == 2){
      searchEmp.value.begin = newValue[0]
      searchEmp.value.end = newValue[1]
      }else {
      searchEmp.value.begin = ''
      searchEmp.value.end = ''
      }
      }
      )

      onMounted(async () => {
      search()

      //加载所有部门数据
      const result = await queryAllDeptApi();
      if(result.code){
      deptList.value = result.data
      }
      })

      //查询员工
      const search= async () => {
      console.log('Search:', searchEmp.value)
      const result = await queryPageApi(searchEmp.value.name, searchEmp.value.gender, searchEmp.value.begin, searchEmp.value.end, currentPage.value, pageSize.value);
      if(result.code){
      empList.value = result.data.rows
      total.value = result.data.total
      }
      }

      const clear = () => {
      // 清空表单
      searchEmp.value = {
      name: '',
      gender: '',
      date: [],
      begin: '',
      end: ''
      }
      search()
      }

      // 示例数据
      const empList = ref([])

      // 分页配置
      const currentPage = ref(1)
      const pageSize = ref(10)
      const total = ref(0)


      // 分页处理
      const handleSizeChange = (val) => {
      search()
      }
      const handleCurrentChange = (val) => {
      search()
      }


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

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

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

      //文件上传
      // 图片上传成功后触发
      const handleAvatarSuccess = (response,uploadFile) => {
      employee.value.image = response.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>

      <template>
      <h1>员工管理</h1> <br>
      <el-form :inline="true" :model="searchEmp">
      <el-form-item label="姓名">
      <el-input v-model="searchEmp.name" placeholder="请输入员工姓名"></el-input>
      </el-form-item>

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

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

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

      <el-button type="primary" @click="addEmp"> + 新增员工</el-button>
      <el-button type="danger" @click=""> - 批量删除</el-button>
      <br><br>

      <!-- 表格 -->
      <el-table :data="empList" border style="width: 100%">
      <el-table-column type="selection" width="55" align="center"></el-table-column>
      <el-table-column prop="name" label="姓名" width="120" align="center"></el-table-column>
      <el-table-column label="性别" width="80" align="center">
      <template #default="scope" >
      {{ scope.row.gender == 1 ? '男' : '女' }}
      </template>
      </el-table-column>
      <el-table-column label="头像" width="170" align="center">
      <template #default="scope" >
      <img :src="scope.row.image" alt="Avatar" class="avatar" />
      </template>
      </el-table-column>
      <el-table-column prop="deptName" label="部门名称" width="170" align="center"></el-table-column>
      <el-table-column 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>
      <el-table-column prop="updateTime" label="最后操作时间" width="210" align="center"></el-table-column>
      <el-table-column label="操作" fixed="right" align="center">
      <template #default="scope">
      <el-button size="small" type="primary" @click="">编辑</el-button>
      <el-button size="small" type="danger" @click="">删除</el-button>
      </template>
      </el-table-column>
      </el-table>

      <br>

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

      <!-- 新增/修改员工的对话框 -->
      <el-dialog v-model="dialogVisible" :title="dialogTitle">
      <el-form ref="employeeFormRef" :model="employee" label-width="80px">
      <!-- 基本信息 -->
      <!-- 第一行 -->
      <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 v-for="gender in genders" :key="gender.name" :label="gender.name" :value="gender.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="job in jobs" :key="job.name" :label="job.name" :value="job.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="dept in deptList" :key="dept.id" :label="dept.name" :value="dept.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="">+ 添加工作经历</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: 1px dashed var(--el-border-color);
      }
      </style>

      到此呢,页面的基本布局我们就已经完成了。 那接下来呢,我们就可以打开浏览器,看看最终的效果:

      添加/删除工作经历

      需求

      新增员工的基本信息表单已经制作完成了,那接下来,要制作的是员工的工作经历。

      点击 **“添加工作经历” **按钮后,新添加一条工作经历;点击每一条后面的 “删除” 按钮,需要删除当前工作经历。

      员工的过往工作经历可能是多条,点击 “添加员工工作经历” 按钮,如何增加一个条目 ? 点击每一条后面的删除按钮,需要删除当前条件?

      • Vue 是基于数据驱动视图展示的。
      • “添加” 时,我们可以往数组中添加数据。
      • “删除” 时,可以删除数组中的元素。
      • 一旦数据发生变化,视图中的展示就会发生变化。
      实现

      1). 在 src/views/emp/index.vue 中的 <script> </script> 中增加如下代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      //工作经历
      //动态添加工作经历 .
      const addExprItem = () => {
      employee.value.exprList.push({exprDate: [], begin: '', end: '', company: '', job: ''})
      }

      //动态删除工作经历 .
      const delExprItem = (index) => {
      employee.value.exprList.splice(index, 1)
      }

      //监听-employee员工对象中的工作经历数据
      watch(()=>employee.value.exprList, (newValue, oldValue) => {
      if(employee.value.exprList && employee.value.exprList.length > 0) {
      employee.value.exprList.forEach(expr => {
      expr.begin = expr.exprDate[0]
      expr.end = expr.exprDate[1]
      })
      }
      }, {deep: true});

      2). 在 src/views/emp/index.vue 中的 <template> </template> 中的按钮绑定事件,并遍历展示工作经历信息:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      <!-- 新增/修改员工的对话框 -->
      <el-dialog v-model="dialogVisible" :title="dialogTitle">
      <el-form ref="employeeFormRef" :model="employee" label-width="80px">
      <!-- 基本信息 -->
      <!-- 第一行 -->
      <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 v-for="gender in genders" :key="gender.name" :label="gender.name" :value="gender.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="job in jobs" :key="job.name" :label="job.name" :value="job.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="dept in deptList" :key="dept.id" :label="dept.name" :value="dept.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="addExprItem">+ 添加工作经历</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.exprDate" 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="delExprItem(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="">保存</el-button>
      </span>
      </template>

      </el-dialog>

      到目前为止,src/views/emp/index.vue 页面的内容如下所示:

      image-20260611091420941

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
      282
      283
      284
      285
      286
      287
      288
      289
      290
      291
      292
      293
      294
      295
      296
      297
      298
      299
      300
      301
      302
      303
      304
      305
      306
      307
      308
      309
      310
      311
      312
      313
      314
      315
      316
      317
      318
      319
      320
      321
      322
      323
      324
      325
      326
      327
      328
      329
      330
      331
      332
      333
      334
      335
      336
      337
      338
      339
      340
      341
      342
      343
      344
      345
      346
      347
      348
      349
      350
      351
      352
      353
      354
      355
      356
      357
      358
      359
      360
      361
      362
      363
      364
      365
      366
      367
      368
      369
      370
      371
      372
      373
      374
      375
      376
      377
      378
      379
      380
      381
      382
      383
      384
      385
      386
      387
      388
      389
      390
      391
      392
      393
      394
      395
      396
      397
      398
      399
      400
      401
      402
      <script setup>
      import { ref, watch, onMounted } from 'vue'
      import { ElMessage } from 'element-plus'
      import { queryPageApi } from '@/api/emp'
      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 deptList = ref([])

      const searchEmp = ref({
      name: '',
      gender: '',
      date: [],
      begin: '',
      end: ''
      })

      //侦听searchEmp中的date属性
      watch(
      () => searchEmp.value.date,
      (newValue, oldValue) => {
      if(newValue.length == 2){
      searchEmp.value.begin = newValue[0]
      searchEmp.value.end = newValue[1]
      }else {
      searchEmp.value.begin = ''
      searchEmp.value.end = ''
      }
      }
      )

      onMounted(async () => {
      search()

      //加载所有部门数据
      const result = await queryAllDeptApi();
      if(result.code){
      deptList.value = result.data
      }
      })

      //查询员工
      const search= async () => {
      console.log('Search:', searchEmp.value)
      const result = await queryPageApi(searchEmp.value.name, searchEmp.value.gender, searchEmp.value.begin, searchEmp.value.end, currentPage.value, pageSize.value);
      if(result.code){
      empList.value = result.data.rows
      total.value = result.data.total
      }
      }

      const clear = () => {
      // 清空表单
      searchEmp.value = {
      name: '',
      gender: '',
      date: [],
      begin: '',
      end: ''
      }
      search()
      }

      // 示例数据
      const empList = ref([])

      // 分页配置
      const currentPage = ref(1)
      const pageSize = ref(10)
      const total = ref(0)


      // 分页处理
      const handleSizeChange = (val) => {
      search()
      }
      const handleCurrentChange = (val) => {
      search()
      }


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

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

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

      //文件上传
      // 图片上传成功后触发
      const handleAvatarSuccess = (response,uploadFile) => {
      employee.value.image = response.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 addExprItem = () => {
      employee.value.exprList.push({exprDate: [], begin: '', end: '', company: '', job: ''})
      }

      //动态删除工作经历 .
      const delExprItem = (index) => {
      employee.value.exprList.splice(index, 1)
      }

      //监听-employee员工对象中的工作经历数据
      watch(()=>employee.value.exprList, (newValue, oldValue) => {
      if(employee.value.exprList && employee.value.exprList.length > 0) {
      employee.value.exprList.forEach(expr => {
      expr.begin = expr.exprDate[0]
      expr.end = expr.exprDate[1]
      })
      }
      }, {deep: true});
      </script>

      <template>
      <h1>员工管理</h1> <br>
      <el-form :inline="true" :model="searchEmp">
      <el-form-item label="姓名">
      <el-input v-model="searchEmp.name" placeholder="请输入员工姓名"></el-input>
      </el-form-item>

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

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

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

      <el-button type="primary" @click="addEmp"> + 新增员工</el-button>
      <el-button type="danger" @click=""> - 批量删除</el-button>
      <br><br>

      <!-- 表格 -->
      <el-table :data="empList" border style="width: 100%">
      <el-table-column type="selection" width="55" align="center"></el-table-column>
      <el-table-column prop="name" label="姓名" width="120" align="center"></el-table-column>
      <el-table-column label="性别" width="80" align="center">
      <template #default="scope" >
      {{ scope.row.gender == 1 ? '男' : '女' }}
      </template>
      </el-table-column>
      <el-table-column label="头像" width="170" align="center">
      <template #default="scope" >
      <img :src="scope.row.image" alt="Avatar" class="avatar" />
      </template>
      </el-table-column>
      <el-table-column prop="deptName" label="部门名称" width="170" align="center"></el-table-column>
      <el-table-column 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>
      <el-table-column prop="updateTime" label="最后操作时间" width="210" align="center"></el-table-column>
      <el-table-column label="操作" fixed="right" align="center">
      <template #default="scope">
      <el-button size="small" type="primary" @click="">编辑</el-button>
      <el-button size="small" type="danger" @click="">删除</el-button>
      </template>
      </el-table-column>
      </el-table>

      <br>

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

      <!-- 新增/修改员工的对话框 -->
      <el-dialog v-model="dialogVisible" :title="dialogTitle">
      <el-form ref="employeeFormRef" :model="employee" label-width="80px">
      <!-- 基本信息 -->
      <!-- 第一行 -->
      <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 v-for="gender in genders" :key="gender.name" :label="gender.name" :value="gender.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="job in jobs" :key="job.name" :label="job.name" :value="job.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="dept in deptList" :key="dept.id" :label="dept.name" :value="dept.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="addExprItem">+ 添加工作经历</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.exprDate" 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="delExprItem(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="">保存</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: 1px dashed var(--el-border-color);
      }
      </style>

      接下来,我们就可以打开浏览器看到具体的页面效果了。

      页面交互

      保存员工信息

      基本的页面布局,我们完成之后,接下来,就需要完成页面的交互操作。 当点击 “保存” 按钮,需要执行如下操作:

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

      具体操作如下:

      1). 为 “保存” 按钮绑定事件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      <!-- ........ 前面的代码省略了......... -->
      <!-- 底部按钮 -->
      <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>

      2). 在 中定义函数 save

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      //保存员工信息
      const save = async () => {
      const result = await addApi(employee.value);
      if(result.code){
      ElMessage.success('新增员工成功')
      dialogVisible.value = false
      search()
      }else {
      ElMessage.error(result.msg)
      }
      }

      然后我们就可以打开浏览器,测试添加员工了。

      表单校验

      从页面原型中的需求描述,我们就可以提取出这个表单的校验规则。 我们可以打开页面原型来看一下:

      通过页面原型,我们可以提取到的校验规则如下:

      表单校验规则:

      • 用户名:必填,长度 2-20
      • 姓名:必填,长度 2-10
      • 性别:必填
      • 手机号:必填,符合手机号规则

      接下来,我们可以借助于 AI,帮我们生成对应的校验规则:

      [!TIP]
      AI 提示词(prompt):
      请帮我生成 ElementPlus 中表单的校验规则,具体规则如下:

      1. 用户名: 必填, 长度 2-20
      2. 姓名: 必填, 长度 2-10
      3. 性别: 必填
      4. 手机号: 必填, 11 位, 符合手机号规则

      请帮我生成表单的校验规则, 并为表单项绑定对应的校验规则。

      参考 AI 生成的代码,并在其基础上进行改造完成新增员工表单的校验。最终代码形式如下(新增代码为绿色背景):

      image-20260611094941867

      image-20260611095108571

      通过ref引用表单校验

      image-20260611095153215

      image-20260611095216368

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
      282
      283
      284
      285
      286
      287
      288
      289
      290
      291
      292
      293
      294
      295
      296
      297
      298
      299
      300
      301
      302
      303
      304
      305
      306
      307
      308
      309
      310
      311
      312
      313
      314
      315
      316
      317
      318
      319
      320
      321
      322
      323
      324
      325
      326
      327
      328
      329
      330
      331
      332
      333
      334
      335
      336
      337
      338
      339
      340
      341
      342
      343
      344
      345
      346
      347
      348
      349
      350
      351
      352
      353
      354
      355
      356
      357
      358
      359
      360
      361
      362
      363
      364
      365
      366
      367
      368
      369
      370
      371
      372
      373
      374
      375
      376
      377
      378
      379
      380
      381
      382
      383
      384
      385
      386
      387
      388
      389
      390
      391
      392
      393
      394
      395
      396
      397
      398
      399
      400
      401
      402
      403
      404
      405
      406
      407
      408
      409
      410
      411
      412
      413
      414
      415
      416
      417
      418
      419
      420
      421
      422
      423
      424
      425
      426
      427
      428
      429
      430
      431
      432
      433
      434
      435
      436
      437
      438
      439
      440
      441
      442
      443
      444
      445
      446
      447
      448
      449
      450
      451
      452
      453
      <script setup>
      import { ref, watch, onMounted } from 'vue'
      import { ElMessage } from 'element-plus'
      import { queryPageApi, addApi } from '@/api/emp'
      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 deptList = ref([])

      const searchEmp = ref({
      name: '',
      gender: '',
      date: [],
      begin: '',
      end: ''
      })

      //侦听searchEmp中的date属性
      watch(
      () => searchEmp.value.date,
      (newValue, oldValue) => {
      if(newValue.length == 2){
      searchEmp.value.begin = newValue[0]
      searchEmp.value.end = newValue[1]
      }else {
      searchEmp.value.begin = ''
      searchEmp.value.end = ''
      }
      }
      )

      onMounted(async () => {
      search()

      //加载所有部门数据
      const result = await queryAllDeptApi();
      if(result.code){
      deptList.value = result.data
      }
      })

      //查询员工
      const search = async () => {
      console.log('Search:', searchEmp.value)
      const result = await queryPageApi(searchEmp.value.name, searchEmp.value.gender, searchEmp.value.begin, searchEmp.value.end, currentPage.value, pageSize.value);
      if(result.code){
      empList.value = result.data.rows
      total.value = result.data.total
      }
      }

      const clear = () => {
      // 清空表单
      searchEmp.value = {
      name: '',
      gender: '',
      date: [],
      begin: '',
      end: ''
      }
      search()
      }

      // 示例数据
      const empList = ref([])

      // 分页配置
      const currentPage = ref(1)
      const pageSize = ref(10)
      const total = ref(0)

      // 分页处理
      const handleSizeChange = (val) => {
      search()
      }
      const handleCurrentChange = (val) => {
      search()
      }


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

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

      //清空表单内容及校验提示信息
      employee.value = {
      username: '',
      name: '',
      gender: '',
      phone: '',
      job: '',
      salary: '',
      deptId: '',
      entryDate: '',
      image: '',
      exprList: []
      }
      if(employeeFormRef.value){
      employeeFormRef.value.resetFields()
      }
      }

      //表单校验规则
      // 验证规则
      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\d{10}$/g, message: '请输入有效的手机号', trigger: 'blur' }
      ]
      });

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

      //文件上传
      // 图片上传成功后触发
      const handleAvatarSuccess = (response,uploadFile) => {
      employee.value.image = response.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 addExprItem = () => {
      employee.value.exprList.push({exprDate: [], begin: '', end: '', company: '', job: ''})
      }

      //动态删除工作经历 .
      const delExprItem = (index) => {
      employee.value.exprList.splice(index, 1)
      }

      //监听-employee员工对象中的工作经历数据
      watch(employee, (newVal, oldVal) => {
      if(employee.value.exprList) {
      employee.value.exprList.forEach(expr => {
      expr.begin = expr.exprDate[0]
      expr.end = expr.exprDate[1]
      })
      }
      }, {deep: true})

      //保存员工信息
      const save = async () => {
      employeeFormRef.value.validate(async valid => {
      if(valid){ // 校验通过
      const result = await addApi(employee.value);
      if(result.code){
      ElMessage.success('新增员工成功')
      dialogVisible.value = false
      search()
      }else {
      ElMessage.error(result.msg)
      }
      }
      })
      }
      </script>

      <template>
      <h1>员工管理</h1> <br>
      <el-form :inline="true" :model="searchEmp">
      <el-form-item label="姓名">
      <el-input v-model="searchEmp.name" placeholder="请输入员工姓名"></el-input>
      </el-form-item>

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

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

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

      <el-button type="primary" @click="addEmp"> + 新增员工</el-button>
      <el-button type="danger" @click=""> - 批量删除</el-button>
      <br><br>

      <!-- 表格 -->
      <el-table :data="empList" border style="width: 100%">
      <el-table-column type="selection" width="55" align="center"></el-table-column>
      <el-table-column prop="name" label="姓名" width="120" align="center"></el-table-column>
      <el-table-column label="性别" width="80" align="center">
      <template #default="scope" >
      {{ scope.row.gender == 1 ? '男' : '女' }}
      </template>
      </el-table-column>
      <el-table-column label="头像" width="170" align="center">
      <template #default="scope" >
      <img :src="scope.row.image" alt="Avatar" class="avatar" />
      </template>
      </el-table-column>
      <el-table-column prop="deptName" label="部门名称" width="170" align="center"></el-table-column>
      <el-table-column 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>
      <el-table-column prop="updateTime" label="最后操作时间" width="210" align="center"></el-table-column>
      <el-table-column label="操作" fixed="right" align="center">
      <template #default="scope">
      <el-button size="small" type="primary" @click="">编辑</el-button>
      <el-button size="small" type="danger" @click="">删除</el-button>
      </template>
      </el-table-column>
      </el-table>

      <br>

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

      <!-- 新增/修改员工的对话框 -->
      <el-dialog v-model="dialogVisible" :title="dialogTitle">
      <el-form ref="employeeFormRef" :model="employee" :rules="rules" label-width="80px">
      <!-- 基本信息 -->
      <!-- 第一行 -->
      <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="gender in genders" :key="gender.name" :label="gender.name" :value="gender.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="job in jobs" :key="job.name" :label="job.name" :value="job.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="dept in deptList" :key="dept.id" :label="dept.name" :value="dept.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="addExprItem">+ 添加工作经历</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.exprDate" 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="delExprItem(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>
      .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: 1px dashed var(--el-border-color);
      }
      </style>

      代码编写完毕后,我们可以打开浏览器测试一下。

      我们可以看到,我们为表单添加的校验规则,确实生效了。 并且,如果表单校验不通过,是不能保存员工信息的。

      18-前端 Web 实战(Tlias 案例-员工管理)

      在昨天的课程中,我们完成了员工管理的查询、新增功能开发。 那今天呢,我们继续来完成员工管理的功能开发,而我们今天要来完成功能包括:

      • 修改员工
      • 删除员工
      • 登录退出
      • 前端打包部署

      修改员工

      对于修改功能,分为两步实现:

      1. 点击 “编辑” 根据 ID 查询员工的信息,回显展示。
      2. 点击 “保存” 按钮,修改员工的信息 。

      查询回显

      1). 为 “编辑” 按钮绑定事件

      1
      2
      3
      4
      5
      6
      <el-table-column label="操作" align="center">
      <template #default="scope">
      <el-button type="primary" size="small" @click="edit(scope.row.id)"><el-icon><EditPen /></el-icon> 编辑</el-button>
      <el-button type="danger" size="small" @click=""><el-icon><Delete /></el-icon> 删除</el-button>
      </template>
      </el-table-column>

      2). 在 中定义 edit函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      //编辑
      const edit = async (id) => {
      const result = await queryInfoApi(id);
      if(result.code){
      dialogVisible.value = true;
      dialogTitle.value = '修改员工';
      employee.value = result.data;

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

      这里需要根据 id 查询员工信息,所以此时要 import 导入 queryInfoApi

      1
      import { queryPageApi, addEmpApi, queryInfoApi } from '@/api/emp'

      打开浏览器,点击 编辑 按钮,测试数据回显:

      修改员工

      完成了数据回显展示之后,接下来,我们就来完成保存修改操作。 由于修改员工与新增员工共用一个 Dialog 对话框。 点击保存按钮时,我们只需要根据 id 来判别是新增员工,还是修改员工。

      那我们就需要对保存员工的函数,进行完善优化。 最终代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      //保存员工
      const save = async () => {
      //表单校验
      if(!empFormRef.value) return;
      empFormRef.value.validate(async (valid) => { //valid 表示是否校验通过: true 通过 / false 不通过
      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('表单校验不通过');
      }
      })
      }

      打开浏览器,测试修改员工信息操作:

      到目前为止,src/views/emp/index.vue 中完整的代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
      282
      283
      284
      285
      286
      287
      288
      289
      290
      291
      292
      293
      294
      295
      296
      297
      298
      299
      300
      301
      302
      303
      304
      305
      306
      307
      308
      309
      310
      311
      312
      313
      314
      315
      316
      317
      318
      319
      320
      321
      322
      323
      324
      325
      326
      327
      328
      329
      330
      331
      332
      333
      334
      335
      336
      337
      338
      339
      340
      341
      342
      343
      344
      345
      346
      347
      348
      349
      350
      351
      352
      353
      354
      355
      356
      357
      358
      359
      360
      361
      362
      363
      364
      365
      366
      367
      368
      369
      370
      371
      372
      373
      374
      375
      376
      377
      378
      379
      380
      381
      382
      383
      384
      385
      386
      387
      388
      389
      390
      391
      392
      393
      394
      395
      396
      397
      398
      399
      400
      401
      402
      403
      404
      405
      406
      407
      408
      409
      410
      411
      412
      413
      414
      415
      416
      417
      418
      419
      420
      421
      422
      423
      424
      425
      426
      427
      428
      429
      430
      431
      432
      433
      434
      435
      436
      437
      438
      439
      440
      441
      442
      443
      444
      445
      446
      447
      448
      449
      450
      451
      452
      453
      454
      455
      456
      457
      458
      459
      460
      461
      462
      463
      464
      465
      466
      467
      468
      469
      470
      471
      472
      473
      474
      475
      476
      477
      478
      479
      480
      481
      482
      483
      484
      485
      486
      487
      488
      489
      490
      491
      492
      <script setup>
      import { ref, watch, onMounted } from 'vue';
      import { queryPageApi, addApi, queryInfoApi, updateApi, deleteApi } from '@/api/emp';
      import { queryAllApi as queryAllDeptApi } from '@/api/dept';
      import { ElMessage, ElMessageBox } from 'element-plus'

      //元数据
      //职位列表数据
      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 searchEmp = ref({name: '', gender: '', date: [], begin: '', end: ''})

      //侦听searchEmp的date属性
      watch(() => searchEmp.value.date, (newVal, oldVal) => {
      if(newVal.length == 2){
      searchEmp.value.begin = newVal[0];
      searchEmp.value.end = newVal[1];
      }else {
      searchEmp.value.begin = '';
      searchEmp.value.end = '';
      }
      })

      //钩子函数
      onMounted(() => {
      search(); //查询员工列表数据
      queryAllDepts();//查询所有部门列表数据
      })

      //查询所有部门数据
      const queryAllDepts = async () => {
      const result = await queryAllDeptApi();
      if(result.code){
      depts.value = result.data;
      }
      }

      //查询员工列表
      const search = async () => {
      const result = await queryPageApi(searchEmp.value.name, searchEmp.value.gender,
      searchEmp.value.begin, searchEmp.value.end, currentPage.value, pageSize.value);
      if(result.code){
      empList.value = result.data.rows;
      total.value = result.data.total;
      }
      }

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

      //员工列表数据
      const empList = ref([])

      //分页
      const currentPage = ref(1); //页码
      const pageSize = ref(10); //每页展示记录数
      const background = ref(true); //背景色
      const total = ref(0); //总记录数

      //每页展示记录数变化
      const handleSizeChange = (val) => {
      search();
      }
      //页码变化时触发
      const handleCurrentChange = (val) => {
      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 employee = ref({
      username: '',
      name: '',
      gender: '',
      phone: '',
      job: '',
      salary: '',
      deptId: '',
      entryDate: '',
      image: '',
      exprList: []
      })

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

      // 文件上传
      // 图片上传成功后触发
      const handleAvatarSuccess = (response) => {
      employee.value.image = response.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 addExprItem = () => {
      employee.value.exprList.push({company: '', job: '', begin: '', end: '', exprDate: []});
      }
      //删除工作经历
      const delExprItem = (index) => {
      employee.value.exprList.splice(index,1);
      }
      //侦听-employee员工对象中的工作经历信息
      watch(() => employee.value.exprList, (newVal, oldVal) => {
      if(employee.value.exprList && employee.value.exprList.length > 0){
      employee.value.exprList.forEach((expr) => {
      expr.begin = expr.exprDate[0];
      expr.end = expr.exprDate[1];
      })
      }
      }, {deep: true}) //深度侦听

      //保存员工
      const save = async () => {
      //表单校验
      if(!empFormRef.value) return;
      empFormRef.value.validate(async (valid) => { //valid 表示是否校验通过: true 通过 / false 不通过
      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 empFormRef = 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' },
      /**
      * 正则表达式: / ..... / ; ^ : 以...开始 ; $ : 以 ... 结束
      * [3-9] : 范围 3-9 之间
      * \d : 数字, [0-9]
      * {9} : 量词
      */
      { pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号', trigger: 'blur' }
      ]
      });

      //编辑
      const edit = async (id) => {
      const result = await queryInfoApi(id);
      if(result.code){
      dialogVisible.value = true;
      dialogTitle.value = '修改员工';
      employee.value = result.data;

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


      </script>

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

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

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

      <el-form-item label="入职时间">
      <el-date-picker
      v-model="searchEmp.date"
      type="daterange"
      range-separator="到"
      start-placeholder="开始日期"
      end-placeholder="结束日期"
      value-format="YYYY-MM-DD"
      />
      </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="deleteByIds">- 批量删除</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">
      {{ scope.row.gender == 1 ? '男' : '女' }}
      </template>
      </el-table-column>
      <el-table-column label="头像" width="120" align="center">
      <template #default="scope">
      <img :src="scope.row.image" height="30px">
      </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">
      <el-button type="primary" size="small" @click="edit(scope.row.id)"><el-icon><EditPen /></el-icon> 编辑</el-button>
      <el-button type="danger" size="small" @click=""><el-icon><Delete /></el-icon> 删除</el-button>
      </template>
      </el-table-column>
      </el-table>
      </div>

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

      <!-- 新增员工/修改员工的对话框 -->
      <el-dialog v-model="dialogVisible" :title="dialogTitle">
      <el-form :model="employee" :rules="rules" ref="empFormRef" label-width="80px">
      <!-- 基本信息 -->
      <!-- 第一行 -->
      <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="addExprItem">+ 添加工作经历</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.exprDate" 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="delExprItem(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: 10px 0px;
      }

      .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>

      删除员工

      在删除员工信息时,有两个操作入口:

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

      删除单个

      1). 为 “删除” 按钮 绑定事件

      1
      2
      3
      4
      5
      6
      <el-table-column label="操作" fixed="right" align="center">
      <template #default="scope">
      <el-button size="small" type="primary" @click="handleEdit(scope.row.id)">编辑</el-button>
      <el-button size="small" type="danger" @click="deleteById(scope.row.id)">删除</el-button>
      </template>
      </el-table-column>

      2). 在 中定义函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      //删除员工
      const deleteById = (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.info('您已取消删除');
      })
      }

      这里用到了 ElmentPlus 中的消息确认框 ElMessageBox,就记得需要引入进来这个组件。

      1
      import { ElMessage, ElMessageBox } from 'element-plus'

      打开浏览器,测试一下:

      批量删除

      1). 为复选框绑定事件 @selection-change

      image-20260611101025485

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      <!-- 数据展示表格 -->
      <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">
      {{ scope.row.gender == 1 ? '男' : '女' }}
      </template>
      </el-table-column>
      <el-table-column label="头像" width="120" align="center">
      <template #default="scope">
      <img :src="scope.row.image" height="30px">
      </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">
      <el-button type="primary" size="small" @click="edit(scope.row.id)"><el-icon><EditPen /></el-icon> 编辑</el-button>
      <el-button type="danger" size="small" @click="deleteById(scope.row.id)"><el-icon><Delete /></el-icon> 删除</el-button>
      </template>
      </el-table-column>
      </el-table>
      </div>

      **2). 定义 handleSelectionChange **函数

      1
      2
      3
      4
      5
      6
      //记录勾选的员工的id
      const selectedIds = ref([]);
      //复选框勾选发生变化时触发 - selection: 当前选中的记录 (数组)
      const handleSelectionChange = (selection) => {
      selectedIds.value = selection.map( item => item.id);
      }

      3). 为 “批量删除” 按钮绑定事件

      1
      2
      <el-button type="primary" @click="addEmp"> + 新增员工</el-button>
      <el-button type="danger" @click="deleteByIds"> - 批量删除</el-button>

      4). 在 中定义函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      //批量删除
      const deleteByIds = () => {
      //弹出确认框
      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.info('您已取消删除');
      })
      }

      打开浏览器,测试一下:

      到此呢,关于员工管理的基本的增删改查功能,我们已经完成了。 目前为止,src/views/emp/index.vue 的完整代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
      282
      283
      284
      285
      286
      287
      288
      289
      290
      291
      292
      293
      294
      295
      296
      297
      298
      299
      300
      301
      302
      303
      304
      305
      306
      307
      308
      309
      310
      311
      312
      313
      314
      315
      316
      317
      318
      319
      320
      321
      322
      323
      324
      325
      326
      327
      328
      329
      330
      331
      332
      333
      334
      335
      336
      337
      338
      339
      340
      341
      342
      343
      344
      345
      346
      347
      348
      349
      350
      351
      352
      353
      354
      355
      356
      357
      358
      359
      360
      361
      362
      363
      364
      365
      366
      367
      368
      369
      370
      371
      372
      373
      374
      375
      376
      377
      378
      379
      380
      381
      382
      383
      384
      385
      386
      387
      388
      389
      390
      391
      392
      393
      394
      395
      396
      397
      398
      399
      400
      401
      402
      403
      404
      405
      406
      407
      408
      409
      410
      411
      412
      413
      414
      415
      416
      417
      418
      419
      420
      421
      422
      423
      424
      425
      426
      427
      428
      429
      430
      431
      432
      433
      434
      435
      436
      437
      438
      439
      440
      441
      442
      443
      444
      445
      446
      447
      448
      449
      450
      451
      452
      453
      454
      455
      456
      457
      458
      459
      460
      461
      462
      463
      464
      465
      466
      467
      468
      469
      470
      471
      472
      473
      474
      475
      476
      477
      478
      479
      480
      481
      482
      483
      484
      485
      486
      487
      488
      489
      490
      491
      492
      493
      494
      495
      496
      497
      498
      499
      500
      501
      502
      503
      504
      505
      506
      507
      508
      509
      510
      511
      512
      513
      514
      515
      516
      517
      518
      519
      520
      521
      522
      523
      524
      525
      526
      527
      528
      529
      530
      531
      532
      533
      534
      535
      536
      537
      <script setup>
      import { ref, watch, onMounted } from 'vue';
      import { queryPageApi, addApi, queryInfoApi, updateApi, deleteApi } from '@/api/emp';
      import { queryAllApi as queryAllDeptApi } from '@/api/dept';
      import { ElMessage, ElMessageBox } from 'element-plus'

      //元数据
      //职位列表数据
      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 searchEmp = ref({name: '', gender: '', date: [], begin: '', end: ''})

      //侦听searchEmp的date属性
      watch(() => searchEmp.value.date, (newVal, oldVal) => {
      if(newVal.length == 2){
      searchEmp.value.begin = newVal[0];
      searchEmp.value.end = newVal[1];
      }else {
      searchEmp.value.begin = '';
      searchEmp.value.end = '';
      }
      })

      //钩子函数
      onMounted(() => {
      search(); //查询员工列表数据
      queryAllDepts();//查询所有部门列表数据
      })

      //查询所有部门数据
      const queryAllDepts = async () => {
      const result = await queryAllDeptApi();
      if(result.code){
      depts.value = result.data;
      }
      }

      //查询员工列表
      const search = async () => {
      const result = await queryPageApi(searchEmp.value.name, searchEmp.value.gender,
      searchEmp.value.begin, searchEmp.value.end, currentPage.value, pageSize.value);
      if(result.code){
      empList.value = result.data.rows;
      total.value = result.data.total;
      }
      }

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

      //员工列表数据
      const empList = ref([])

      //分页
      const currentPage = ref(1); //页码
      const pageSize = ref(10); //每页展示记录数
      const background = ref(true); //背景色
      const total = ref(0); //总记录数

      //每页展示记录数变化
      const handleSizeChange = (val) => {
      search();
      }
      //页码变化时触发
      const handleCurrentChange = (val) => {
      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 employee = ref({
      username: '',
      name: '',
      gender: '',
      phone: '',
      job: '',
      salary: '',
      deptId: '',
      entryDate: '',
      image: '',
      exprList: []
      })

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

      // 文件上传
      // 图片上传成功后触发
      const handleAvatarSuccess = (response) => {
      employee.value.image = response.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 addExprItem = () => {
      employee.value.exprList.push({company: '', job: '', begin: '', end: '', exprDate: []});
      }
      //删除工作经历
      const delExprItem = (index) => {
      employee.value.exprList.splice(index,1);
      }
      //侦听-employee员工对象中的工作经历信息
      watch(() => employee.value.exprList, (newVal, oldVal) => {
      if(employee.value.exprList && employee.value.exprList.length > 0){
      employee.value.exprList.forEach((expr) => {
      expr.begin = expr.exprDate[0];
      expr.end = expr.exprDate[1];
      })
      }
      }, {deep: true}) //深度侦听

      //保存员工
      const save = async () => {
      //表单校验
      if(!empFormRef.value) return;
      empFormRef.value.validate(async (valid) => { //valid 表示是否校验通过: true 通过 / false 不通过
      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 empFormRef = 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' },
      /**
      * 正则表达式: / ..... / ; ^ : 以...开始 ; $ : 以 ... 结束
      * [3-9] : 范围 3-9 之间
      * \d : 数字, [0-9]
      * {9} : 量词
      */
      { pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号', trigger: 'blur' }
      ]
      });

      //编辑
      const edit = async (id) => {
      const result = await queryInfoApi(id);
      if(result.code){
      dialogVisible.value = true;
      dialogTitle.value = '修改员工';
      employee.value = result.data;

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

      //删除员工
      const deleteById = (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.info('您已取消删除');
      })
      }
      //记录勾选的员工的id
      const selectedIds = ref([]);
      //复选框勾选发生变化时触发 - selection: 当前选中的记录 (数组)
      const handleSelectionChange = (selection) => {
      selectedIds.value = selection.map( item => item.id);
      }

      //批量删除
      const deleteByIds = () => {
      //弹出确认框
      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.info('您已取消删除');
      })
      }

      </script>

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

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

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

      <el-form-item label="入职时间">
      <el-date-picker
      v-model="searchEmp.date"
      type="daterange"
      range-separator="到"
      start-placeholder="开始日期"
      end-placeholder="结束日期"
      value-format="YYYY-MM-DD"
      />
      </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="deleteByIds">- 批量删除</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">
      {{ scope.row.gender == 1 ? '男' : '女' }}
      </template>
      </el-table-column>
      <el-table-column label="头像" width="120" align="center">
      <template #default="scope">
      <img :src="scope.row.image" height="30px">
      </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">
      <el-button type="primary" size="small" @click="edit(scope.row.id)"><el-icon><EditPen /></el-icon> 编辑</el-button>
      <el-button type="danger" size="small" @click="deleteById(scope.row.id)"><el-icon><Delete /></el-icon> 删除</el-button>
      </template>
      </el-table-column>
      </el-table>
      </div>

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

      <!-- 新增员工/修改员工的对话框 -->
      <el-dialog v-model="dialogVisible" :title="dialogTitle">
      <el-form :model="employee" :rules="rules" ref="empFormRef" label-width="80px">
      <!-- 基本信息 -->
      <!-- 第一行 -->
      <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="addExprItem">+ 添加工作经历</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.exprDate" 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="delExprItem(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: 10px 0px;
      }

      .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>

      登录退出

      员工的增删改查功能我们完成之后,最后我们再来完成一个功能,那就是登录退出功能。

      登录页面所在位置为:src/views/login/index.vue,基本的页面布局,我们已经写好了。

      我们可以在浏览器中访问路径:http://localhost:5173/login 访问到登录页面。

      那接下来呢,我们就先来完成登录功能。

      登录

      image-20260611103156085

      导入userouter调用该函数,返回router实例,拿到实例后通过push指跳转的路径,

      image-20260611103456450

      1). 定义登录请求的 api : src/api/login.js

      1
      2
      3
      4
      import request from '@/utils/request'

      //登录
      export const loginApi = (data) => request.post('/login', data)

      2). 在 src/views/login/index.vue 中增加如下代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      <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:''})
      let router = useRouter()

      //登录
      const login = async () => {
      const result = await loginApi(loginForm.value)
      if (result.code) {// 登录成功
      ElMessage.success('登录成功')
      localStorage.setItem('loginUser', JSON.stringify(result.data))
      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>

      在登录成功之后,需要记录用户的登录信息,这里我们可以直接将用户的登录信息记录在浏览器端的 localStorage 本地存储中,以后需要使用登录的相关信息,直接从 localStorage 中取出即可。

      [!TIP]

      • localStorage 是浏览器提供的本地存储机制 (5MB)。
      • 存储形式为 key-value 形式,键和值都是字符串类型。
      • API 方法:
        • localStorage.setItem(key, value)
        • localStorage.getItem(key)
        • localStorage.removeItem(key)
        • localStorage.clear()

      携带令牌访问

      在后续的每一次 Ajax 请求中都获取 localStorage 中的令牌,在请求头中将令牌携带到服务端。 如果这么做,就会非常麻烦,需要在每一次 ajax 请求的时候获取中存储在 localStorage 中的令牌,并携带到服务端,是非常繁琐的。

      我们可以思考一下,目前我们项目的 Ajax 请求,是不是都是基于 Axios 发送的,而在我们的项目中,我们是自己定义了一个 axios 的请求对象 request。所有的异步请求,是不都是基于 request 对象发起的。 所以,这里呢,我们可以借助于 axios 中提供的拦截器来进行统一处理。

      接下来,我们就需要在 request.js 中通过 axios 的拦截器实现此功能。具体代码实现如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      import axios from 'axios'

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

      //axios的请求 request 拦截器, 每次请求获取localStorage中的loginUser, 从中获取到token, 在请求头token中携带到服务端
      request.interceptors.request.use(
      (config) => {
      let loginUser = JSON.parse(localStorage.getItem('loginUser'))
      console.log(localStorage.getItem('loginUser'))
      if (loginUser) {
      config.headers.token = loginUser.token
      }
      return config
      }
      )

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

      export default request

      响应 401 跳转到登录页面

      目前,即使用户未登录的情况下访问服务器,服务器会响应 401 状态码,但是前端并不会跳转到登录页面。 因为,我们在前端并未做任何的拦截判断。接下来,我们就来实现此功能,我们只需要在服务端将数据响应给前端时,在 axios 的响应拦截器中统一判断处理即可。

      接下来,我们就需要在 request.js 中通过 axios 的响应拦截器实现此功能。具体代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      import axios from 'axios'
      import { ElMessage } from 'element-plus'
      import router from '../router'

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

      //axios的请求 request 拦截器, 每次请求获取localStorage中的loginUser, 从中获取到token, 在请求头token中携带到服务端
      request.interceptors.request.use(
      (config) => {
      let loginUser = JSON.parse(localStorage.getItem('loginUser'))
      console.log(localStorage.getItem('loginUser'))
      if (loginUser) {
      config.headers.token = loginUser.token
      }
      return config
      }
      )

      //axios的响应 response 拦截器
      request.interceptors.response.use(
      (response) => { //成功回调
      return response.data
      },
      (error) => { //失败回调
      //如果响应的状态码为401, 则路由到登录页面
      if (error.response.status === 401) {
      ElMessage.error('登录失效, 请重新登录')
      router.push('/login')
      }else{
      ElMessage.success('接口访问异常')
      }
      return Promise.reject(error)
      }
      )

      export default request

      展示登录用户

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      <script setup>
      import {ref, onMounted} from 'vue'
      const loginName = ref('')
      //定义钩子函数, 获取登录用户名
      onMounted(() => {
      //获取登录用户名
      let loginUser = JSON.parse(localStorage.getItem('loginUser'))
      if (loginUser) {
      loginName.value = loginUser.name
      }
      })
      </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> 退出登录 【{{ loginName }}】
      </a>
      </span>
      </el-header>

      <el-container>
      <!-- 左侧菜单 -->
      <el-aside width="200px" class="aside">

      <el-menu router>
      <!-- 首页菜单 -->
      <el-menu-item index="/index">
      <el-icon><Promotion /></el-icon> 首页
      </el-menu-item>

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

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

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

      我们打开浏览器,完成登录之后,就可以看到,在退出登录之后,就显示出了当前登录员工的姓名。效果如下:

      退出

      让超链接失效变成死链接

      image-20260611105305437

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      <script setup>
      import {ref, onMounted} from 'vue'
      import { ElMessage, ElMessageBox } from 'element-plus';
      import {useRouter} from 'vue-router'

      let router = useRouter()

      const loginName = ref('')
      //定义钩子函数, 获取登录用户名
      onMounted(() => {
      //获取登录用户名
      let loginUser = JSON.parse(localStorage.getItem('loginUser'))
      if (loginUser) {
      loginName.value = loginUser.name
      }
      })

      const logout = () => {
      //弹出确认框, 如果确认, 则退出登录, 跳转到登录页面
      ElMessageBox.confirm('确认退出登录吗?', '提示', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
      }).then(() => {//确认, 则清空登录信息
      ElMessage.success('退出登录成功')
      localStorage.removeItem('loginUser')
      router.push('/login')//跳转到登录页面
      })
      }
      </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="javascript:void(0)" @click="logout">
      <el-icon><SwitchButton /></el-icon> 退出登录 【{{ loginName }}】
      </a>
      </span>
      </el-header>

      <el-container>
      <!-- 左侧菜单 -->
      <el-aside width="200px" class="aside">

      <el-menu router>
      <!-- 首页菜单 -->
      <el-menu-item index="/index">
      <el-icon><Promotion /></el-icon> 首页
      </el-menu-item>

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

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

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

      我们打开浏览器,完成登录之后,就可以看到,在退出登录之后,就显示出了当前登录员工的姓名。然后点击退出登录,就可以看到效果如下:

      点击确定之后,就会退出登录,跳转到登录页面。

      图片上传 401 问题

      目前,我们已经完成了部门管理,员工管理,以及登录退出等所有的功能。 但是其实目前程序中还存在一点小问题,那就是图片上传的时候,服务器端响应 401。如下图:

      出现这个问题的原因是因为图片上传我们使用的是 ElementPlus 中提供的 el-upload 组件,请求服务端的时候并未通过 axios,所以也并不会通过我们配置的 axios 的拦截器,携带请求头 token 了。 服务器端,拦截到请求之后,发现未携带令牌,就会出现这个问题。

      那接下来,针对于这个文件上传的请求,我们则需要单独处理一下。处理思路如下:

      • 页面加载完毕后,从 localStorage 中获取登录员工信息,然后获取到 token 令牌。
      • 然后在文件上传时,在请求头中将令牌携带到服务端。

      具体操作步骤:

      1). 在 src/views/emp/index.vue 中的 <script></script> 添加如下代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      //声明token
      const token = ref('')

      //钩子函数
      onMounted(() => {
      search(); //查询员工列表数据
      queryAllDepts();//查询所有部门列表数据
      getToken(); //获取token
      })

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

      1). 在 src/views/emp/index.vue 中的 <template></template> 添加如下代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      <!-- 第五行 -->
      <el-row :gutter="20">
      <el-col :span="24">
      <el-form-item label="头像">
      <el-upload
      class="avatar-uploader"
      action="/api/upload"
      :headers="{'token': token}"
      :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>

      这样,这个问题就解决了,我们打开浏览器,测试一下文件上传。我们看到,在文件上传时,在请求头中携带了 token 令牌。

      打包部署

      到此呢,部门管理、员工管理、登录认证的功能,我们都已经完成了。 那接下来,我们就来说一下前端工程的打包部署。 前端项目最终开发完毕之后,是需要打包,然后部署在 nginx 服务器上运行的 。

      打包

      直接双击 npm 脚本中的 build 即可将项目打包,打包后的文件会出现在 dist 目录中。

      部署

      • Nginx

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

      官网:https://nginx.org/

      • 部署

      打包完成之后,就可以将打包后的项目,部署到 nginx 服务器上了,记得将 nginx 解压到一个没有中文不带空格的目录中 。

      然后直接将 dist 目录中的内容,拷贝到 nginx 的解压目录中的 html 中即可 (html 目录下原有的两个文件, 可以直接删除)。

      image-20260611105809661

      然后,在 nginx 服务器的核心配置文件 conf/nginx 中,在 http 配置块里面 添加如下反向代理的配置:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      server {
      listen 80;
      server_name localhost;
      client_max_body_size 10m;
      #默认路径
      location / {
      root html;
      index index.html index.htm;
      try_files $uri $uri/ /index.html;
      }
      #如果请求路径以/api开头,rewrite会对路径重写
      location ^~ /api/ {
      #路径重写的规则是将/api去掉只保留(.*)$ /$1 ,去掉之后再交给proxy_pass 后端tomcat服务器
      rewrite ^/api/(.*)$ /$1 break;
      proxy_pass http://localhost:8080;
      }

      error_page 500 502 503 504 /50x.html;
      location = /50x.html {
      root html;
      }
      }

      然后就可以双击 nginx.exe 启动项目了。 访问 http://localhost

      [!TIP]
      Nginx 默认占用 80 端口号,如果 80 端口号被占用,可以在 nginx.conf 中修改端口号。(netstat –ano | findStr 80)

      Nginx 服务器启动、重载、停止的相关命令:

      • 启动:nginx.exe
      • 重载:nginx.exe -s reload
      • 停止:nginx.exe -s stop

      image-20260611110526910