勇敢向前 發表於 2024-3-14 20:46:00

Tlias-前端开发

<h1 id="tlias">Tlias</h1>
<p><img src="https://euneiropnrenia-1320529117.cos.ap-nanjing.myqcloud.com/sandox/pic/20231223092239.png"></p>
<h2 id="准备工作">准备工作</h2>
<p><img src="https://euneiropnrenia-1320529117.cos.ap-nanjing.myqcloud.com/sandox/pic/20231223092257.png"></p>
<ul>
<li>安装依赖</li>
</ul>
<pre><code class="language-bash">npm install element-plus --save
npm install axios
</code></pre>
<ul>
<li>配置ElementPlus</li>
</ul>
<pre><code class="language-ts">//main.ts

import { createApp } from 'vue'
import { createPinia } from 'pinia'

import App from './App.vue'
import router from './router'
import './assets/main.css'

//导入elementPlus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

const app = createApp(App)


//注册ElementPlus的Icon组件
for (const of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
}

app.use(createPinia())
app.use(router)
app.use(ElementPlus, {locale: zhCn})

app.mount('#app')



//env.d.ts
declare module 'element-plus/dist/locale/zh-cn.mjs';
</code></pre>
<h2 id="页面布局">页面布局</h2>
<p><img src="https://euneiropnrenia-1320529117.cos.ap-nanjing.myqcloud.com/sandox/pic/20231223104241.png"></p>
<p>公共的css属性可以定义在main.css中:</p>
<pre><code class="language-css">main.css

*{
margin: 0;
}
</code></pre>
<h3 id="container">Container</h3>
<p>布局需要使用Container布局容器:</p>
<ul>
<li><code>&lt;el-container&gt;</code> : 外层容器</li>
<li><code>&lt;el-header&gt;</code> : 顶栏容器</li>
<li><code>&lt;el-aside&gt;</code> :侧边栏容器</li>
<li><code>&lt;el-container&gt;</code>:主要区域容器</li>
<li><code>&lt;el-footer&gt;</code> : 底栏容器</li>
</ul>
<pre><code class="language-vue">&lt;!--IndexView.vue--&gt;
&lt;template&gt;
    &lt;div class="common-layout"&gt;
       &lt;el-container&gt;
          &lt;el-header class="header"&gt; &lt;HeaderComponent/&gt; &lt;/el-header&gt;
          &lt;el-container&gt;
             &lt;el-aside width="200px" class="aside"&gt;
                &lt;AsideComponent/&gt;
             &lt;/el-aside&gt;
             &lt;el-main&gt;Main&lt;/el-main&gt;
          &lt;/el-container&gt;
       &lt;/el-container&gt;
    &lt;/div&gt;
&lt;/template&gt;
</code></pre>
<h3 id="header">Header</h3>
<p><img src="https://euneiropnrenia-1320529117.cos.ap-nanjing.myqcloud.com/sandox/pic/20231223104607.png"></p>
<pre><code class="language-vue">&lt;!--HeaderComponent.vue--&gt;
&lt;script setup lang="ts"&gt;

&lt;/script&gt;

&lt;template&gt;
    &lt;span class="title"&gt;Tlias智能学习辅助系统&lt;/span&gt;
      
    &lt;span class="right_tool"&gt;
          &lt;a href=""&gt;
            &lt;!--图标--&gt;
            &lt;el-icon&gt;&lt;EditPen /&gt;&lt;/el-icon&gt; 修改密码 &amp;nbsp;&amp;nbsp;&amp;nbsp; |&amp;nbsp;&amp;nbsp;&amp;nbsp;
          &lt;/a&gt;
          &lt;a href=""&gt;
            &lt;el-icon&gt;&lt;SwitchButton /&gt;&lt;/el-icon&gt; 退出登录
          &lt;/a&gt;
      &lt;/span&gt;
&lt;/template&gt;

&lt;style scoped&gt;
.title {
    color: white;
    font-size: 40px;
    font-family: 楷体;
    line-height: 60px;
    }

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

a {
    color: white;
    text-decoration: none;
    }
&lt;/style&gt;
</code></pre>
<p>修改密码和退出登录 需要使用ElementPlus提供的图标,官网提供的使用方式:</p>
<blockquote>
<p>需要从 <code>@element-plus/icons-vue</code> 中导入所有图标并进行全局注册。</p>
</blockquote>
<pre><code class="language-ts">//main.ts

// 如果您正在使用CDN引入,请删除下面一行。
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

const app = createApp(App)
for (const of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
</code></pre>
<p>在图标合集中选择图标就可以直接使用:</p>
<pre><code class="language-vue">    &lt;a href=""&gt;
            &lt;el-icon&gt;&lt;SwitchButton /&gt;&lt;/el-icon&gt; 退出登录
&lt;/a&gt;
</code></pre>
<h3 id="aside">Aside</h3>
<p><img src="https://euneiropnrenia-1320529117.cos.ap-nanjing.myqcloud.com/sandox/pic/20231223103350.png"></p>
<pre><code class="language-vue">   &lt;el-aside width="200px"&gt;
      &lt;el-scrollbar&gt;
      &lt;el-menu :default-openeds="['1', '3']"&gt;
      &lt;!--el-sub-menu是一个子菜单--&gt;
          &lt;el-sub-menu index="1"&gt;
            &lt;template #title&gt;
            &lt;el-icon&gt;&lt;message /&gt;&lt;/el-icon&gt;Navigator One
            &lt;/template&gt;
            &lt;!--el-menu-item-group是子菜单的一组--&gt;
            &lt;el-menu-item-group&gt;
            &lt;template #title&gt;Group 1&lt;/template&gt;
            &lt;!--el-menu-item是一个菜单项--&gt;
            &lt;el-menu-item index="1-1"&gt;Option 1&lt;/el-menu-item&gt;
            &lt;el-menu-item index="1-2"&gt;Option 2&lt;/el-menu-item&gt;
            &lt;/el-menu-item-group&gt;
            &lt;el-menu-item-group title="Group 2"&gt;
            &lt;el-menu-item index="1-3"&gt;Option 3&lt;/el-menu-item&gt;
            &lt;/el-menu-item-group&gt;
            &lt;el-sub-menu index="1-4"&gt;
            &lt;template #title&gt;Option4&lt;/template&gt;
            &lt;el-menu-item index="1-4-1"&gt;Option 4-1&lt;/el-menu-item&gt;
            &lt;/el-sub-menu&gt;
          &lt;/el-sub-menu&gt;
          &lt;el-sub-menu index="2"&gt;
            &lt;template #title&gt;
            &lt;el-icon&gt;&lt;icon-menu /&gt;&lt;/el-icon&gt;Navigator Two
            &lt;/template&gt;
            &lt;el-menu-item-group&gt;
            &lt;template #title&gt;Group 1&lt;/template&gt;
            &lt;el-menu-item index="2-1"&gt;Option 1&lt;/el-menu-item&gt;
            &lt;el-menu-item index="2-2"&gt;Option 2&lt;/el-menu-item&gt;
            &lt;/el-menu-item-group&gt;
            &lt;el-menu-item-group title="Group 2"&gt;
            &lt;el-menu-item index="2-3"&gt;Option 3&lt;/el-menu-item&gt;
            &lt;/el-menu-item-group&gt;
            &lt;el-sub-menu index="2-4"&gt;
            &lt;template #title&gt;Option 4&lt;/template&gt;
            &lt;el-menu-item index="2-4-1"&gt;Option 4-1&lt;/el-menu-item&gt;
            &lt;/el-sub-menu&gt;
          &lt;/el-sub-menu&gt;
          &lt;el-sub-menu index="3"&gt;
            &lt;template #title&gt;
            &lt;el-icon&gt;&lt;setting /&gt;&lt;/el-icon&gt;Navigator Three
            &lt;/template&gt;
            &lt;el-menu-item-group&gt;
            &lt;template #title&gt;Group 1&lt;/template&gt;
            &lt;el-menu-item index="3-1"&gt;Option 1&lt;/el-menu-item&gt;
            &lt;el-menu-item index="3-2"&gt;Option 2&lt;/el-menu-item&gt;
            &lt;/el-menu-item-group&gt;
            &lt;el-menu-item-group title="Group 2"&gt;
            &lt;el-menu-item index="3-3"&gt;Option 3&lt;/el-menu-item&gt;
            &lt;/el-menu-item-group&gt;
            &lt;el-sub-menu index="3-4"&gt;
            &lt;template #title&gt;Option 4&lt;/template&gt;
            &lt;el-menu-item index="3-4-1"&gt;Option 4-1&lt;/el-menu-item&gt;
            &lt;/el-sub-menu&gt;
          &lt;/el-sub-menu&gt;
      &lt;/el-menu&gt;
      &lt;/el-scrollbar&gt;
    &lt;/el-aside&gt;
</code></pre>
<p>当前项目的需求:</p>
<p><img src="https://euneiropnrenia-1320529117.cos.ap-nanjing.myqcloud.com/sandox/pic/20231223103731.png"></p>
<p>四个子菜单,没有分组</p>
<h3 id="main">Main</h3>
<p>配置嵌套路由:</p>
<pre><code class="language-ts">const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
      path : '/',
      name : 'home',
      component : () =&gt; import('../views/layout/IndexView.vue'),
      children : [ //嵌套路由
      {
          path : 'index',
          name : 'index',
          component : () =&gt; import('../views/index/WelcomePageIndex.vue')
      },
      {
          path : 'emp',
          name : 'emp',
          component : () =&gt; import('../views/emp/EmpIndex.vue')
      },
      {
          path : 'dept',
          name : 'dept',
          component : () =&gt; import('../views/dept/DeptIndex.vue')
      },
      {
          path : 'clazz',
          name : 'clazz',
          component : () =&gt; import('../views/clazz/ClazzIndex.vue')
      },
      ]
    })
export default router
</code></pre>
<p>App.vue:</p>
<pre><code class="language-vue">&lt;script setup lang="ts"&gt;

&lt;/script&gt;

&lt;template&gt;
        &lt;!--IndexView--&gt;
    &lt;RouterView/&gt;
&lt;/template&gt;

&lt;style scoped&gt;

&lt;/style&gt;
</code></pre>
<p>IndexView.vue:</p>
<pre><code class="language-vue">&lt;template&gt;
    &lt;div class="common-layout"&gt;
       &lt;el-container&gt;
          &lt;el-header class="header"&gt; &lt;HeaderComponent/&gt; &lt;/el-header&gt;
          &lt;el-container&gt;
             &lt;el-aside width="200px" class="aside"&gt;
                &lt;AsideComponent/&gt;
             &lt;/el-aside&gt;
             &lt;el-main&gt; &lt;RouterView/&gt; &lt;/el-main&gt;
          &lt;/el-container&gt;
       &lt;/el-container&gt;
    &lt;/div&gt;
&lt;/template&gt;
</code></pre>
<pre><code class="language-vue">&lt;el-scrollbar&gt;
&lt;el-menu router&gt;
   &lt;!-- 首页菜单 --&gt;
   &lt;!--启用vue-router模式,将index作为path进行跳转--&gt;
   &lt;el-menu-item index="/index"&gt;
    &lt;el-icon&gt;&lt;Promotion /&gt;&lt;/el-icon&gt; 首页
   &lt;/el-menu-item&gt;

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

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

   &lt;!-- 数据统计管理 --&gt;
   &lt;el-sub-menu index="/report"&gt;
    &lt;template #title&gt;
   &lt;el-icon&gt;&lt;Histogram /&gt;&lt;/el-icon&gt;数据统计管理
    &lt;/template&gt;
    &lt;el-menu-item index="/empReport"&gt;
   &lt;el-icon&gt;&lt;InfoFilled /&gt;&lt;/el-icon&gt;员工信息统计
    &lt;/el-menu-item&gt;
    &lt;el-menu-item index="/stuReport"&gt;
   &lt;el-icon&gt;&lt;Share /&gt;&lt;/el-icon&gt;学员信息统计
    &lt;/el-menu-item&gt;
    &lt;el-menu-item index="/log"&gt;
   &lt;el-icon&gt;&lt;Document /&gt;&lt;/el-icon&gt;日志信息统计
    &lt;/el-menu-item&gt;
   &lt;/el-sub-menu&gt;
&lt;/el-menu&gt;
&lt;/el-scrollbar&gt;
</code></pre>
<p>但是当前直接访问系统的界面:</p>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231224220743.png"></p>
<p>因为默认的请求路径是:<code>http://127.0.0.1:5173/</code>,路由能匹配到IndexView.vue,匹配不到IndexView内部的RouterView,所以只渲染了IndexView</p>
<p>解决办法:对路由 <code>/</code> 进行重定向:</p>
<pre><code class="language-ts">{
path : '/',
name : 'home',
component : () =&gt; import('../views/layout/IndexView.vue'),
redirect : '/index',
children : [ //嵌套路由
    {
      path : 'index',
      name : 'index',
      component : () =&gt; import('../views/index/WelcomePageIndex.vue')
    }
}
</code></pre>
<p>访问 / 就会访问到index</p>
<h2 id="部门管理功能实现">部门管理功能实现</h2>
<h3 id="查询所有">查询所有</h3>
<h4 id="页面布局-1">页面布局</h4>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231224221218.png"></p>
<p>需要的组件:Button、Table</p>
<pre><code class="language-vue">&lt;script setup lang="ts"&gt;
import {ref} from "vue";
//声明表格的数据模型
let deptList = ref([]);
&lt;/script&gt;

&lt;template&gt;
&lt;h1&gt;部门管理&lt;/h1&gt;

&lt;el-button type="primary"&gt;+ 新增部门&lt;/el-button&gt;

&lt;el-table :data="deptList" border style="width: 100%"&gt;
    &lt;el-table-column prop="date" label="Date" width="180" /&gt;
    &lt;el-table-column prop="name" label="Name" width="180" /&gt;
    &lt;el-table-column prop="address" label="Address" /&gt;
&lt;/el-table&gt;
&lt;/template&gt;

&lt;style scoped&gt;

&lt;/style&gt;
</code></pre>
<p>但是我们目前使用的是ts,对于<strong>ref可以指定泛型,用来规定其中存储的数据类型</strong>,而deptList是请求服务器返回的数据,接口文档中规定了响应数据的格式:</p>
<pre><code class="language-json">{
"code": 1,
"msg": "success",
"data": [
    {
      "id": 1,
      "name": "学工部",
      "createTime": "2022-09-01T23:06:29",
      "updateTime": "2022-09-01T23:06:29"
    },
    {
      "id": 2,
      "name": "教研部",
      "createTime": "2022-09-01T23:06:29",
      "updateTime": "2022-09-01T23:06:29"
    }
]
}
</code></pre>
<p>此处的泛型就是数组类型,数组中存储的元素类型是我们定义的:</p>
<pre><code class="language-ts">interface deptModel{
        id?: number,
        name: string,
        updateTime?: string
}
</code></pre>
<ul>
<li>没有定义createTime:前端不需要展示createTime</li>
<li>updateTime和id定义为可选参数,因为dept不仅只有查询的部门,也会有新增的部门(新增的部门没有id和更新时间),这是交给后端定义的字段</li>
</ul>
<p>定义泛型:</p>
<pre><code class="language-ts">//声明部门的数据类型
interface deptModel{
id?: number,
name: string,
updateTime?: string
}

//声明表格的数据模型
//泛型是deptModel类型的数组
let deptList = ref&lt;deptModel[]&gt;([]);
</code></pre>
<p>一般会将所有的泛型和类型别名定义在单独的ts文件中,一般在api/model/model.ts中:</p>
<pre><code class="language-ts">//api/model/model.ts

// ----------------------- 部门数据相关接口及类型 ---------------------
//部门数据接口
// ? 新增
export interface DeptModel {
id?: number,
name: string,
updateTime?: string
}

//部门数据数组的类型别名
export type DeptModelArray = DeptModel[]
</code></pre>
<p>在需要的地方引入即可:</p>
<pre><code class="language-ts">import {ref} from "vue";

//引入类型/接口需要使用type关键字
import type {DeptModelArray} from "../../api/model/model";

//声明表格的数据模型
//泛型是deptModel类型的数组
let deptList = ref&lt;DeptModelArray&gt;([]);
</code></pre>
<p>在此处引入DeptModelArray的时候,需要回退两级目录,可以用@代表根目录src,直接在根目录下引入:</p>
<pre><code class="language-ts">//@代表src目录
import type {DeptModelArray} from "@/api/model/model";
</code></pre>
<p>接下来继续完善表格的数据显示,界面原型显示需要四列:</p>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231224223704.png"></p>
<pre><code class="language-vue">&lt;el-table :data="deptList" border style="width: 100%"&gt;
&lt;el-table-column prop="date" label="序号" width="180" align="center"/&gt;
&lt;el-table-column prop="name" label="部门名称" width="180" align="center"/&gt;
&lt;el-table-column prop="address" label="最后操作时间" align="center"/&gt;
&lt;el-table-column prop="address" label="操作" align="center"/&gt;
&lt;/el-table&gt;
</code></pre>
<p>prop指定的是属性名,而属性名在 数据类型接口 interface DeptModel中指定了:</p>
<pre><code class="language-ts">//api/model/model.ts

// ----------------------- 部门数据相关接口及类型 ---------------------
//部门数据接口
// ? 新增
export interface DeptModel {
id?: number,
name: string,
updateTime?: string
}

//部门数据数组的类型别名
export type DeptModelArray = DeptModel[]
</code></pre>
<p>其中的序号并不是id属性,ElementPlus给出了显示序号的解决办法:设置 <code>type</code> 属性为 <code>index</code> 即可显示从 1 开始的索引号。</p>
<pre><code class="language-vue">&lt;el-table :data="deptList" border style="width: 100%"&gt;
&lt;el-table-column prop="" label="序号" width="180" align="center"&gt;
    &lt;template #default="scope"&gt;
      
    &lt;/template&gt;
&lt;/el-table-column&gt;
&lt;el-table-column prop="name" label="部门名称" width="180" align="center"/&gt;
&lt;el-table-column prop="updateTime" label="最后操作时间" align="center"/&gt;
&lt;el-table-column prop="" label="操作" align="center"&gt;
    &lt;template #default="scope"&gt;

    &lt;/template&gt;
&lt;/el-table-column&gt;
</code></pre>
<h4 id="加载数据">加载数据</h4>
<p>需求:</p>
<ol>
<li>增删改完毕后,加载最新的部门数据</li>
<li>打开页面后,加载最新的部门数据</li>
</ol>
<p>定义查询部门列表的函数:</p>
<pre><code class="language-ts">//查询部门列表
const search = async ()=&gt; {
let promise = await axios.get('/api/depts');
//返回了一个Promise对象,data字段封装了响应的数据,在后端是Result格式的JSON字符串
console.log(promise)
//promise.data 是Result,再.data是结果
deptList.value = promise.data.data;
}

onMounted(() =&gt; {
search();
});
</code></pre>
<blockquote>
<p>在后端没有开发好的情况下,可以使用Apifox的Mock功能:</p>
</blockquote>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231224231424.png"></p>
<p>复制链接作为get方法的入参就可以进行测试了</p>
<p>search方法最好加一个判断,根据Result的code字段进行判断:</p>
<pre><code class="language-ts">//查询部门列表
const search = async () =&gt; {
let promise = await axios.get('https://mock.apifox.com/m1/3708703-0-default/depts');
//返回了一个Promise对象,data字段封装了响应的数据,在后端是Result格式的JSON字符串
console.log(promise)

if (promise.data.code) {
    //promise.data 是Result,再.data是结果
    deptList.value = promise.data.data;
}

}

onMounted(() =&gt; {
search();
});
</code></pre>
<p>当前访问的是服务器的接口,需要进行跨域的处理:</p>
<pre><code class="language-ts">//vite.config.ts

export default defineConfig({
plugins: [
    vue(),
    vueJsx(),
],
resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
},
//跨域
server: {
    cors: true,
    open: true,
    port: 5173,
    proxy: {
      '^/api': {
      target: 'http://localhost:8080/',
      changeOrigin: true,
      //需要对/api的/进行转义
      rewrite: (path) =&gt; path.replace(/^\/api/, '')
      }
    }
}
</code></pre>
<p>以上配置的含义是,匹配到/api开始的请求都将目的地址改为:http://localhost:8080/api/path,以/api/dept为例:</p>
<pre><code class="language-bash">axios.get('/api/dept') -拦截请求-&gt; http://localhost:8080/api/dept -/api替换为空字符串-&gt; http://localhost:8080/dept
</code></pre>
<p>所以请求的方法可以直接请求/api/dept:</p>
<pre><code class="language-ts">//查询部门列表
const search = async () =&gt; {
let promise = await axios.get('/api/depts');
//返回了一个Promise对象,data字段封装了响应的数据,在后端是Result格式的JSON字符串
console.log(promise)

if (promise.data.code) {
    //promise.data 是Result,再.data是结果
    deptList.value = promise.data.data;
}

}
</code></pre>
<h5 id="初步优化泛型">初步优化:泛型</h5>
<p>但是每次请求都带有/api还是比较繁琐的,并且promise.data是后端的Result对象,每次都要从promise中把Result提取出来再 .data获取数据,提取Result的操作是相同的,可以对程序进行初步优化:</p>
<ul>
<li>封装请求工具类utils/request.ts:</li>
</ul>
<pre><code class="language-ts">const request = axios.create({
        //请求均以/api开始
        baseURL : '/api',
        timeout : 60000
});

//axios的响应response的拦截器
request.interceptors.response.use(
        //成功回调
        (response) =&gt; {
                //提取Result,await request.get()的返回值就是Result对象
                return response.data;
        },
        //失败回调
        (error) =&gt; {
          //拿到错误信息,继续失败回调
                return Promise.reject(error);
        }
);

export default request;
</code></pre>
<blockquote>
<p>/api是为了区分ajax请求,其他请求不需要Tomcat处理</p>
</blockquote>
<p>axios被封装为request对象,请求可以直接通过request发起,响应的数据经过拦截器的提取,<strong>只取出服务器端响应的纯数据</strong>,我们得到的就是Result对象,此时发起请求:</p>
<pre><code class="language-ts">const search = async () =&gt; {
//拦截器提取出Result对象
let dept = await request.get('/depts');
console.log(dept);
if (dept.code){
    deptList.value = dept.data;
}
}
</code></pre>
<p>但是这样做,在ts下会提示错误:</p>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231224233852.png"></p>
<p><strong>因为没有指定get方法的返回值类型为ResultModel,ts无法得知其中是否有code属性</strong></p>
<p>axios的get方法是有泛型的:</p>
<pre><code class="language-ts">get&lt;T&nbsp;=&nbsp;any,&nbsp;R&nbsp;=&nbsp;AxiosResponse&lt;T&gt;,&nbsp;D&nbsp;=&nbsp;any&gt;(url:&nbsp;string,&nbsp;config?:&nbsp;AxiosRequestConfig&lt;D&gt;):&nbsp;Promise&lt;R&gt;;
</code></pre>
<p>axios的get方法实际上是对axios.request的一层封装,request方法:</p>
<pre><code class="language-ts">request&lt;T&nbsp;=&nbsp;any,&nbsp;R&nbsp;=&nbsp;AxiosResponse&lt;T&gt;,&nbsp;D&nbsp;=&nbsp;any&gt;(config:&nbsp;AxiosRequestConfig&lt;D&gt;):&nbsp;Promise&lt;R&gt;;
</code></pre>
<p>request方法有三个泛型,T、R、D,接受AxiosRequestConfig类型的参数作为配置对象,返回值是接受泛型R的Promise类型</p>
<p>R的默认类型 AxiosResponse:</p>
<pre><code class="language-ts">export interface AxiosResponse&lt;T = any, D = any&gt; {
    data: T;
    status: number;
    statusText: string;
    headers: RawAxiosResponseHeaders | AxiosResponseHeaders;
    config: AxiosRequestConfig&lt;D&gt;;
    request?: any;
   }
</code></pre>
<p>AxiosResponse就是响应拦截器用到的response对象的类型:</p>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225000042.png"></p>
<p>T就是服务器端返回的数据的类型,而服务器端返回的类型是不确定的,所以定义为any</p>
<p>再看request方法的定义:</p>
<pre><code class="language-ts">request&lt;T&nbsp;=&nbsp;any,&nbsp;R&nbsp;=&nbsp;AxiosResponse&lt;T&gt;,&nbsp;D&nbsp;=&nbsp;any&gt;(config:&nbsp;AxiosRequestConfig&lt;D&gt;):&nbsp;Promise&lt;R&gt;;
</code></pre>
<ul>
<li>T:服务器返回数据的类型</li>
<li>R:服务器返回的数据经过axios一层封装得到的response对象的类型</li>
</ul>
<p><strong>request方法的返回值是Promise,值就是成功态的R,也就是response对象</strong>。</p>
<pre><code class="language-ts">
{// &lt;- AxiosResponse
        data: {
                code : '',
                msg : '',
                data : any
        },
    status: number,
    statusText: string,
    headers: RawAxiosResponseHeaders | AxiosResponseHeaders,
    config: AxiosRequestConfig&lt;D&gt;,
    request?: any
}
</code></pre>
<p><strong>所以get、post、put方法的返回值都是Promise,值均为成功态的R,也就是response对象</strong></p>
<p>再看我们的封装:</p>
<pre><code class="language-ts">const request = axios.create({
baseURL: '/api',
timeout: 600000
})

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

export default request
</code></pre>
<p>其实就是将response中的data提取出来了,上文中提到data的类型是T=any,这样get请求得到的结果类型一定是T,因为get请求的结果就是Promise,也就是成功态的R,而R已经被我们在拦截器中转换为T了,所以我们可以直接指定T和R的类型:</p>
<pre><code class="language-ts">const search = async () =&gt; {
                                                        //改变了await request.get方法的返回值
let dept = await request.get&lt;ResultModel,ResultModel&gt;('/depts') ;
console.log(dept);
if (dept.code){
    deptList.value = dept.data;
}
}
</code></pre>
<p>此时就不会报错了。</p>
<p><strong>但是这种做法是不正确的</strong>,axios的拦截器可以配置多个,多个拦截器会形成一个拦截器链,每个拦截器链的参数都是AxiosResponse类型,如果在响应回调里直接return response.data,R就变为T了,应该保证每个拦截器的签名一直,否则对下游的拦截器可能产生影响,不建议这样操作,应该将axios的get、put、post方法统一封装,返回最终需要指定的类型。</p>
<h5 id="分层优化">分层优化</h5>
<p>现代前端开发会将<strong>和服务器端交互的逻辑定义在单独的api中</strong>,例如:api/dept.ts</p>
<pre><code class="language-ts">//其实是拦截器将R变为T了,此处才能写ResultModel
export const queryAllDepts = () =&gt; request.get&lt;any,ResultModel&gt;('/depts');
</code></pre>
<p>调用:</p>
<pre><code class="language-ts">const search = async () =&gt; {
//直接调用该函数发送请求即可
//await 拿到的就是成功态的R,拦截器已经将R变为T了
let result = await queryAllDepts();
if (result.code) {
    deptList.value = result.data;
}
}
</code></pre>
<h3 id="新增部门">新增部门</h3>
<p>点击新增部门按钮,弹出Dialog对话框</p>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225012549.png"></p>
<pre><code class="language-vue">&lt;script setup lang="ts"&gt;

//新增部门

// 1. 对话框
let dialogFormVisible = ref&lt;boolean&gt;(false);
// 表单数据,类型限定必须指定name
let dept = ref&lt;DeptModel&gt;({name:''});

// 2. 弹窗
let add = () =&gt; {
dialogFormVisible.value = true;   
}
&lt;/script&gt;

&lt;template&gt;
&lt;h1&gt;部门管理&lt;/h1&gt;

&lt;el-button type="primary" @click="add"&gt;+ 新增部门&lt;/el-button&gt;

&lt;el-table :data="deptList" border style="width: 100%"&gt;
                ...
    &lt;el-table-column prop="" label="操作" align="center"&gt;
      &lt;template #default="scope"&gt;
      &lt;el-button type="success" size="small"&gt;编辑&lt;/el-button&gt;
      &lt;el-button type="danger" size="small"&gt;删除&lt;/el-button&gt;
      &lt;/template&gt;
    &lt;/el-table-column&gt;
&lt;/el-table&gt;

&lt;el-dialog v-model="dialogFormVisible" title="Shipping address"&gt;
    &lt;el-form :model="dept"&gt;
      &lt;el-form-item label="Promotion name" &gt;
      &lt;el-input v-model="dept.name" autocomplete="off" /&gt;
      &lt;/el-form-item&gt;
    &lt;/el-form&gt;
    &lt;template #footer&gt;
      &lt;span class="dialog-footer"&gt;
      &lt;el-button @click="dialogFormVisible = false"&gt;Cancel&lt;/el-button&gt;
      &lt;el-button type="primary" @click="dialogFormVisible = false"&gt;
          Confirm
      &lt;/el-button&gt;
      &lt;/span&gt;
    &lt;/template&gt;
&lt;/el-dialog&gt;
&lt;/template&gt;

&lt;style scoped&gt;

&lt;/style&gt;
</code></pre>
<p>此时的效果:</p>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225013806.png"></p>
<p>对话框的标题不应该直接指定为 新增部门 ,编辑按钮弹出的对话框和这个相同,编辑时标题应该为 修改部门</p>
<p>在add方法中赋值为新增部门,在update方法中赋值为修改部门</p>
<p>title应该是v-bind绑定的。</p>
<p>完成新增功能:</p>
<pre><code class="language-ts">//   api/dept.ts
//接口文档指明参数为dept类型
export const addApi = (dept:DeptModel) =&gt; request.post&lt;any,ResultModel&gt;('/depts',dept)
</code></pre>
<pre><code class="language-vue">&lt;script setup lang="ts"&gt;
//新增部门

// 1. 对话框
let dialogFormVisible = ref&lt;boolean&gt;(false);
// 表单数据,类型限定必须指定name
let dept = ref&lt;DeptModel&gt;({name:''});
// 对话框标题,可能是新增部门/编辑部门
let formTitle = ref&lt;string&gt;('');

// 2. 弹窗
let add = () =&gt; {
//显示对话框
dialogFormVisible.value = true;
//标题赋值
formTitle.value = '新增部门';
}
// 3. 保存
let save = async () =&gt; {
//调用交互层保存数据,数据在dept对象中
//体现了TS的强大之处,此处很容易写为dept
let result = await addApi(dept.value);

//成功关闭弹窗
if (result.code){
    //关闭弹窗
    dialogFormVisible.value = false;
    //提示操作成功
    ElMessage.success('保存成功');
    //列表刷新
    search();
}else {
    //不关闭弹窗:给用户修改的机会
    //提示操作失败
    ElMessage.error(result.msg);
}
}
&lt;/script&gt;

&lt;template&gt;
&lt;h1&gt;部门管理&lt;/h1&gt;

&lt;el-button type="primary" @click="add"&gt;+ 新增部门&lt;/el-button&gt;

&lt;el-table :data="deptList" border style="width: 100%"&gt;
      ...
      &lt;template #default="scope"&gt;
      &lt;el-button type="success" size="small"&gt;编辑&lt;/el-button&gt;
      &lt;el-button type="danger" size="small"&gt;删除&lt;/el-button&gt;
      &lt;/template&gt;
    &lt;/el-table-column&gt;
&lt;/el-table&gt;

&lt;el-dialog v-model="dialogFormVisible" :title="formTitle" width="30%"&gt;
    &lt;el-form :model="dept"&gt;
      &lt;el-form-item label="部门名称" &gt;
      &lt;el-input v-model="dept.name" autocomplete="off" /&gt;
      &lt;/el-form-item&gt;
    &lt;/el-form&gt;
    &lt;template #footer&gt;
      &lt;span class="dialog-footer"&gt;
      &lt;!--取消直接设置为false--&gt;
      &lt;el-button @click="dialogFormVisible = false"&gt;取消&lt;/el-button&gt;
      &lt;!--确认是有逻辑的--&gt;
      &lt;el-button type="primary" @click="save"&gt;
          确定
      &lt;/el-button&gt;
      &lt;/span&gt;
    &lt;/template&gt;
&lt;/el-dialog&gt;
&lt;/template&gt;

&lt;style scoped&gt;

&lt;/style&gt;
</code></pre>
<p>但是还是存在问题的:下一次弹窗还会显示dept.value.name的值,因为这次没有清空数据。</p>
<ul>
<li>应该在何处设置清空dept.value.name?</li>
</ul>
<p>不能在保存成功后清空,如果保存失败用户直接关闭窗口,下一次打开还是原先的数据</p>
<p><strong>应该在弹出对话框时清空</strong></p>
<pre><code class="language-ts">// 2. 弹窗
let add = () =&gt; {
//清空之前的dept.value.name
dept.value.name = '';
//显示对话框
dialogFormVisible.value = true;
//标题赋值
formTitle.value = '新增部门';
}
</code></pre>
<p><strong>在后端的增/删/改也是有必要返回Result的,可以在前端给用户提供信息参考。</strong></p>
<h3 id="修改部门">修改部门</h3>
<p>分为两步:</p>
<ol>
<li>查询回显</li>
<li>保存修改</li>
</ol>
<h4 id="查询回显">查询回显</h4>
<p>点击编辑按钮,需要查询回显,为编辑按钮绑定update回调函数,<strong>需要为其传递参数id</strong></p>
<pre><code class="language-vue">&lt;el-table :data="deptList" border style="width: 100%"&gt;
&lt;el-table-column type="index" label="序号" width="100" align="center"/&gt;
&lt;el-table-column prop="name" label="部门名称" width="250" align="center"/&gt;
&lt;el-table-column prop="updateTime" label="最后操作时间" align="center" width="350"/&gt;
&lt;el-table-column prop="" label="操作" align="center"&gt;
    &lt;template #default="scope"&gt;                   &lt;!--传递id--&gt;
      &lt;el-button type="success" size="small" @click="update(scope.row.id)"&gt;编辑&lt;/el-button&gt;
      &lt;el-button type="danger" size="small"&gt;删除&lt;/el-button&gt;
    &lt;/template&gt;
&lt;/el-table-column&gt;
&lt;/el-table&gt;
</code></pre>
<p>也体现了后端返回给前端的数据是必须带有id的,这样针对某些数据的操作才能让后端辨别数据身份</p>
<pre><code class="language-ts">//三、修改部门
// 1.1 数据回显
const update = async (id:number)=&gt; {
//清空之前的dept.value.name
dept.value.name = '';
//显示对话框
dialogFormVisible.value = true;
//设置标题
formTitle.value = '修改部门';

//dept.value = (await getInfoById(id)).data
//其实byId应该不能是失败的
let result = await getInfoById(id);
if (result.code){
    //直接替换dept对象,替换name TS会报错
    //dept.value.name = result.data.name
    dept.value = result.data;
}
}
</code></pre>
<ul>
<li>注意:<strong>时刻注意接口文档中/类型注解中规定的类型</strong></li>
</ul>
<h4 id="保存修改">保存修改</h4>
<p>点击对话框的保存,触发修改的逻辑,<strong>但是新增部门和修改部门的对话框是同一个</strong>,在新增部门中,我们已经为对话框的保存绑定了save方法:</p>
<pre><code class="language-vue">&lt;el-dialog v-model="dialogFormVisible" :title="formTitle" width="30%"&gt;
&lt;el-form :model="dept"&gt;
    &lt;el-form-item label="部门名称" &gt;
      &lt;el-input v-model="dept.name" autocomplete="off" /&gt;
    &lt;/el-form-item&gt;
&lt;/el-form&gt;
&lt;template #footer&gt;
    &lt;span class="dialog-footer"&gt;
      &lt;!--取消直接设置为false--&gt;
      &lt;el-button @click="dialogFormVisible = false"&gt;取消&lt;/el-button&gt;
      &lt;!--确认是有逻辑的--&gt;
      &lt;el-button type="primary" @click="save"&gt;
      确定
      &lt;/el-button&gt;
    &lt;/span&gt;
&lt;/template&gt;
&lt;/el-dialog&gt;
</code></pre>
<pre><code class="language-ts">let save = async () =&gt; {
//调用交互层保存数据,数据在dept对象中
//体现了TS的强大之处,此处很容易写为dept
let result = await addApi(dept.value);

//成功关闭弹窗
if (result.code){
    //关闭弹窗
    dialogFormVisible.value = false;
    //提示操作成功
    ElMessage.success('保存成功');
    //列表刷新
    search();

}else {
    //不关闭弹窗:给用户修改的机会
    //提示操作失败
    ElMessage.error(result.msg);
}
}
</code></pre>
<p>也就是说,在对话框的save方法中既要完成新增,又要完成修改,先定义交互层的修改方法:</p>
<pre><code class="language-ts">export const modifyInfoApi = (dept:DeptModel) =&gt; request.put&lt;any,ResultModel&gt;('/depts',dept);
</code></pre>
<pre><code class="language-ts">// 保存
let save = async () =&gt; {

let result;
//新增和修改的区别是dept.value的id属性是否有值
if (dept.value.id){
    //有id修改
    result = await modifyInfoApi(dept.value);
}else {
    //无id新增
    result = await addApi(dept.value);
}
//调用交互层保存数据,数据在dept对象中
//体现了TS的强大之处,此处很容易写为dept
    //成功关闭弹窗
if (result.code){
    //关闭弹窗
    dialogFormVisible.value = false;
    //提示操作成功
    ElMessage.success('保存成功');
    //列表刷新
    search();

}else {
    //不关闭弹窗:给用户修改的机会
    //提示操作失败
    ElMessage.error(result.msg);
}
}
</code></pre>
<pre><code class="language-ts">//三、修改部门
const update = async (id:number)=&gt; {
// 1. 数据回显
//清空之前的dept.value.name
dept.value.name = '';
//显示对话框
dialogFormVisible.value = true;
//设置标题
formTitle.value = '修改部门';

dept.value = (await getInfoById(id)).data
//其实byId应该不能是失败的
/*let result = await getInfoById(id);
if (result.code){    //直接替换dept对象,替换name TS会报错
    //dept.value.name = result.data.name    dept.value = result.data;}*/
}
</code></pre>
<h3 id="删除部门">删除部门</h3>
<ul>
<li>
<p>根据id删除,删除完毕刷新页面</p>
</li>
<li>
<p>点击删除之后弹出确认框 ElMessageBox</p>
</li>
</ul>
<pre><code class="language-vue">&lt;template&gt;
&lt;el-button text @click="open"&gt;Click to open the Message Box&lt;/el-button&gt;
&lt;/template&gt;

&lt;script lang="ts" setup&gt;
import { ElMessage, ElMessageBox } from 'element-plus'

const open = () =&gt; {
ElMessageBox.confirm(
    'proxy will permanently delete the file. Continue?',
    'Warning', //警告图标
    { //确认按钮文本
      confirmButtonText: 'OK',
      //取消按钮文本
      cancelButtonText: 'Cancel',
      type: 'warning',
    }
)
    .then(() =&gt; {
      ElMessage({
      type: 'success',
      message: 'Delete completed',
      })
    })
    .catch(() =&gt; {
      ElMessage({
      type: 'info',
      message: 'Delete canceled',
      })
    })
}
&lt;/script&gt;
</code></pre>
<pre><code class="language-ts">// 四、删除部门

const deleteById = (id:number) =&gt; {
//确认是否删除
ElMessageBox.confirm(
      '是否确认删除?',
      'Warning',
      {
      confirmButtonText: '确认',
      cancelButtonText: '取消',
      type: 'warning',
      }
)   //注意async的位置
      .then(async () =&gt; {
      let result = await removeByIdApi(id);
      if (result.code){
          ElMessage({
            type: 'success',
            message: '删除成功',
          })
      }else{
          ElMessage.error(result.msg)
      }
      })
      .catch(() =&gt; {
      ElMessage({
          type: 'info',
          message: '取消删除',
      })
      })
//刷新页面
search();
}
</code></pre>
<h2 id="表单校验">表单校验</h2>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225085408.png"></p>
<p>需要对表单进行校验,ElementPlus给了表单校验的方案:</p>
<p><strong>为rules属性传入约定的验证规则,并且将form-item的prop属性设置为需要验证的特殊键值即可。</strong></p>
<pre><code class="language-vue">&lt;template&gt;
&lt;!--rules属性--&gt;
&lt;el-form
    ref="ruleFormRef"
    :model="ruleForm"
    :rules="rules"
    label-width="120px"
    class="demo-ruleForm"
    :size="formSize"
    status-icon
&gt;                                  &lt;!--设置name属性--&gt;
    &lt;el-form-item label="Activity name" prop="name"&gt;
      &lt;el-input v-model="ruleForm.name" /&gt;
    &lt;/el-form-item&gt;
   
    &lt;el-form-item&gt;
      &lt;el-button type="primary" @click="submitForm(ruleFormRef)"&gt;
      Create
      &lt;/el-button&gt;
      &lt;el-button @click="resetForm(ruleFormRef)"&gt;Reset&lt;/el-button&gt;
    &lt;/el-form-item&gt;
&lt;/el-form&gt;
&lt;/template&gt;

&lt;script lang="ts" setup&gt;
import { reactive, ref } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'

interface RuleForm {
name: string
}

const formSize = ref('default')
const ruleFormRef = ref&lt;FormInstance&gt;()
const ruleForm = reactive&lt;RuleForm&gt;({
name: 'Hello',
})

const rules = reactive&lt;FormRules&lt;RuleForm&gt;&gt;({
name: [            
    { required: true, message: 'Please input Activity name', trigger: 'blur' },
    { min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur' },
]
})

const submitForm = async (formEl: FormInstance | undefined) =&gt; {
if (!formEl) return
await formEl.validate((valid, fields) =&gt; {
    if (valid) {
      console.log('submit!')
    } else {
      console.log('error submit!', fields)
    }
})
}

const resetForm = (formEl: FormInstance | undefined) =&gt; {
if (!formEl) return
formEl.resetFields()
}

const options = Array.from({ length: 10000 }).map((_, idx) =&gt; ({
value: `${idx + 1}`,
label: `${idx + 1}`,
}))
&lt;/script&gt;

</code></pre>
<pre><code class="language-vue">&lt;el-dialog v-model="dialogFormVisible" :title="formTitle" width="30%"&gt;
&lt;!--rules绑定校验规则--&gt;
&lt;el-form
      :model="dept"
      :rules="rules"
&gt;
    &lt;!--prop指定使用哪条校验规则--&gt;
    &lt;el-form-item label="部门名称" prop="name"&gt;
      &lt;el-input v-model="dept.name" autocomplete="off"/&gt;
    &lt;/el-form-item&gt;
&lt;/el-form&gt;
&lt;template #footer&gt;
    &lt;span class="dialog-footer"&gt;
      &lt;el-button @click="dialogFormVisible = false"&gt;取消&lt;/el-button&gt;
      &lt;el-button type="primary" @click="save"&gt;
      确定
      &lt;/el-button&gt;
    &lt;/span&gt;
&lt;/template&gt;
&lt;/el-dialog&gt;
</code></pre>
<p>rules:<strong>FormRules的泛型需要指定针对哪个类型的校验规则</strong>,已经定义了DeptModel可以直接使用</p>
<pre><code class="language-ts">const rules = ref&lt;FormRules&lt;DeptModel&gt;&gt;({
name: [
    { required: true, message: '请输入部门名称', trigger: 'blur' },
    { min: 2, max: 10, message: '部门名称长度在2-10位之间', trigger: 'blur' },
]})
</code></pre>
<ul>
<li>required:必填</li>
<li>message:校验失败的提示信息</li>
<li>triggr:触发校验的事件</li>
</ul>
<p>但是此时的表单虽然校验不通过,点击保存按钮还是可以发起请求的,在save方法中我们应该判断表单校验是否通过,<strong>需要拿到表单的实例</strong>,通过实例进行校验</p>
<p>定义表单的实例引用对象:</p>
<pre><code class="language-ts">const deptForm = ref&lt;FormInstance&gt;();
</code></pre>
<p>保存按钮:</p>
<pre><code class="language-vue">&lt;el-dialog v-model="dialogFormVisible" :title="formTitle" width="30%"&gt;
&lt;el-form
      :model="dept"
      :rules="rules"
&gt;
    &lt;el-form-item label="部门名称" prop="name"&gt;
      &lt;el-input v-model="dept.name" autocomplete="off"/&gt;
    &lt;/el-form-item&gt;
&lt;/el-form&gt;
&lt;template #footer&gt;
    &lt;span class="dialog-footer"&gt;
      &lt;el-button @click="dialogFormVisible = false"&gt;取消&lt;/el-button&gt;
      &lt;!--保存按钮传递表单的校验规则--&gt;
      &lt;el-button type="primary" @click="save(deptForm)"&gt;&lt;!--也可以不定义这个参数--&gt;
      确定
      &lt;/el-button&gt;
    &lt;/span&gt;
&lt;/template&gt;
&lt;/el-dialog&gt;
</code></pre>
<p>save方法进行校验:</p>
<pre><code class="language-ts">let save = async (form:FormInstance | undefined) =&gt; {

if (!form) return
await form.validate(async (valid, fields) =&gt; {
    if (valid) {//valid -&gt; true 校验 通过
      //校验通过
      let result;

      if (dept.value.id) {
      result = await modifyInfoApi(dept.value);
      } else {
      result = await addApi(dept.value);
      }

      if (result.code) {
      dialogFormVisible.value = false;
      ElMessage.success('保存成功');
      search();

      } else {
      ElMessage.error(result.msg);
      }

    } else {
      //校验失败
      ElMessage.error('校验失败,不能提交')
    }
})

}
</code></pre>
<blockquote>
<p>实际上save方法不传递form实例也可以,直接使用</p>
</blockquote>
<p>但是当前还是存在问题的:</p>
<p>用户第一次验证失败后,点击关闭,再次打开弹窗表单中存在的还是上一次的校验错误提示,<strong>表单的状态没有被重置</strong></p>
<p>ElementPlus给出了表单状态重置的方法:</p>
<pre><code class="language-ts">const resetForm = (formEl: FormInstance | undefined) =&gt; {
if (!formEl) return
formEl.resetFields()
}
</code></pre>
<p>根据前文的经验,我们应该在打开表单的时候进行状态重置:</p>
<pre><code class="language-ts">// 2. 弹窗
let add = () =&gt; {
//清空之前的dept.value.name
dept.value.name = '';
//显示对话框
dialogFormVisible.value = true;
//标题赋值
formTitle.value = '新增部门';

resetForm(deptForm.value);
}

const update = async (id: number) =&gt; {
// 1. 数据回显
//清空之前的dept.value.name
dept.value.name = '';
//显示对话框
dialogFormVisible.value = true;
//设置标题
formTitle.value = '修改部门';
   
resetForm(deptForm.value);
   
dept.value = (await getInfoByIdApi(id)).data
}
</code></pre>
<p>可以发现很多代码都是重复的,可以抽取为单独的方法:</p>
<pre><code class="language-ts">//打开对话框的通用操作
const openForm = ()=&gt; {
//清空之前的dept.value.name
dept.value.name = '';
//显示对话框
dialogFormVisible.value = true;
//重置表单状态
resetForm(deptForm.value);
}

const update = async (id: number) =&gt; {
//重置
openForm();
   
//设置标题
formTitle.value = '修改部门';
dept.value = (await getInfoByIdApi(id)).data;
}

let add = () =&gt; {
openForm();
//标题赋值
formTitle.value = '新增部门';
}
</code></pre>
<h2 id="员工管理">员工管理</h2>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225102520.png"></p>
<h3 id="分页查询">分页查询</h3>
<h4 id="页面布局-2">页面布局</h4>
<p><mark><strong>页面布局流程</strong></mark>:</p>
<ul>
<li>确定页面布局时所使用的Element组件</li>
<li>确定涉及到的数据模型(接口、响应式数据)</li>
</ul>
<h5 id="搜索栏">搜索栏</h5>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225102606.png"></p>
<p>如果表单封装的数据较多,建议封装在一个对象中</p>
<p>SearchEmpModel:专门用来封装搜索栏的表单数据</p>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225102617.png"></p>
<p>需要使用ElementPlus提供的日期组件el-date-picker,type=daterange得到的是两个时间:开始时间和结束时间,<strong>这两个时间对应了searchEmp中的一个属性date数组</strong></p>
<pre><code class="language-vue">&lt;script setup lang="ts"&gt;
import {ref} from "vue";
import type {SearchEmpModel} from "@/api/model/model";

let searchEmp = ref&lt;SearchEmpModel&gt;({
    name: '',
    gender : '',
    begin : '',
    end : '',
    date : []
});
&lt;/script&gt;

&lt;template&gt;
&lt;!-- 搜索栏 model指定封装在哪个对象中--&gt;
&lt;el-form :inline="true" :model="searchEmp" class="demo-form-inline"&gt;
    &lt;el-form-item label="姓名"&gt;
      &lt;el-input v-model="searchEmp.name" placeholder="请输入姓名"/&gt;
    &lt;/el-form-item&gt;

    &lt;el-form-item label="性别"&gt;
      &lt;el-select v-model="searchEmp.gender" placeholder="请选择"&gt;
      &lt;el-option label="男" value="1" /&gt;
      &lt;el-option label="女" value="2" /&gt;
      &lt;/el-select&gt;
    &lt;/el-form-item&gt;

    &lt;el-form-item label="入职时间"&gt;
      &lt;el-date-picker
          v-model="searchEmp.date"
          type="daterange"
          range-separator="到"
          start-placeholder="开始时间"
          end-placeholder="结束时间"
          value-format="YYYY-MM-DD"
      /&gt;
    &lt;/el-form-item&gt;

    &lt;el-form-item&gt;
      &lt;el-button type="primary" @click=""&gt;查询&lt;/el-button&gt;
      &lt;el-button @click=""&gt;清空&lt;/el-button&gt;
    &lt;/el-form-item&gt;
&lt;/el-form&gt;

&lt;!-- 功能按钮 --&gt;
&lt;el-button type="success" @click=""&gt;+ 新增员工&lt;/el-button&gt;
&lt;el-button type="danger" @click=""&gt;- 批量删除&lt;/el-button&gt;
&lt;br&gt;&lt;br&gt;

&lt;/template&gt;
</code></pre>
<p>日期数据封装在date数组中,传递给服务器端的数据应该是begin和end,现在需要给begin、end进行赋值</p>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225201959.png"></p>
<p>此处的赋值需要使用[]</p>
<pre><code class="language-ts">watch(() =&gt; searchEmp.value.date, (newValue, oldValue) =&gt; {

        /*   
        if (newValue.length !== 2){
                searchEmp.value.begin = '';      
                searchEmp.value.end = '';      
        }else {      
                searchEmp.value.begin = newValue;      
                searchEmp.value.end = newValue;      
        }*/
        if (newValue.length != 2) {
        newValue.push('', '');
        }
        searchEmp.value.begin = newValue;
        searchEmp.value.end = newValue;

}, {deep: true})
</code></pre>
<h5 id="表格及分页">表格及分页</h5>
<h6 id="表格">表格</h6>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225113922.png"></p>
<pre><code class="language-vue">&lt;!-- 列表展示 --&gt;
&lt;el-table :data="empList" border style="width: 100%" fit &gt;
    &lt;el-table-column prop="name" label="姓名" align="center" width="130px" /&gt;
    &lt;el-table-column prop="gender" label="性别" align="center" width="100px"/&gt;
    &lt;el-table-column prop="image" label="头像" align="center"/&gt;
    &lt;el-table-column prop="deptName" label="所属部门" align="center" /&gt;
    &lt;el-table-column prop="job" label="职位" align="center" width="100px"/&gt;
    &lt;el-table-column prop="entryDate" label="入职时间" align="center" width="130px" /&gt;
    &lt;el-table-column prop="updateTime" label="最后修改时间" align="center" /&gt;
    &lt;el-table-column label="操作" align="center"&gt;
      &lt;template #default="scope"&gt;
      &lt;el-button type="primary" size="small" @click=""&gt;编辑&lt;/el-button&gt;
      &lt;el-button type="danger" size="small" @click=""&gt;删除&lt;/el-button&gt;
      &lt;/template&gt;
    &lt;/el-table-column&gt;
&lt;/el-table&gt;
&lt;br&gt;

&lt;!-- 分页组件Pagination --&gt;
&lt;el-pagination
    v-model:current-page="pagination.currentPage"
    v-model:page-size="pagination.pageSize"
    :page-sizes=""
    layout="total, sizes, prev, pager, next, jumper"
    :total="pagination.total"
    @size-change="handleSizeChange"
    @current-change="handleCurrentChange"
/&gt;
</code></pre>
<p>表格第一列需要一个多选框,实现多选非常简单: 手动添加一个 <code>el-table-column</code>,设 <code>type</code> 属性为 <code>selection</code> 即可。</p>
<p>但是多选框的选中项是要向服务器提交的数据,在选中项变化的时候应该更新数据:</p>
<pre><code class="language-vue">&lt;!-- 列表展示 --&gt;
&lt;el-table
    :data="empList"
    border
    style="width: 100%"
    fit
    @selection-change="handleSelectionChange"
&gt;
&lt;!--多选框--&gt;
&lt;el-table-column type="selection" width="55" /&gt;
&lt;el-table-column prop="name" label="姓名" align="center" width="130px" /&gt;
&lt;el-table-column prop="gender" label="性别" align="center" width="100px"/&gt;
&lt;el-table-column prop="image" label="头像" align="center"/&gt;
&lt;el-table-column prop="deptName" label="所属部门" align="center" /&gt;
&lt;el-table-column prop="job" label="职位" align="center" width="100px"/&gt;
&lt;el-table-column prop="entryDate" label="入职时间" align="center" width="130px" /&gt;
&lt;el-table-column prop="updateTime" label="最后修改时间" align="center" /&gt;
&lt;el-table-column label="操作" align="center"&gt;
    &lt;template #default="scope"&gt;
      &lt;el-button type="primary" size="small" @click=""&gt;编辑&lt;/el-button&gt;
      &lt;el-button type="danger" size="small" @click=""&gt;删除&lt;/el-button&gt;
    &lt;/template&gt;
&lt;/el-table-column&gt;
&lt;/el-table&gt;
</code></pre>
<p>@selection-change指定多选框选中项变化时的回调函数</p>
<h6 id="分页">分页</h6>
<pre><code class="language-vue">&lt;!-- 分页组件Pagination --&gt;
&lt;el-pagination
    v-model:current-page="pagination.currentPage"
    v-model:page-size="pagination.pageSize"
    :page-sizes=""
    layout="total, sizes, prev, pager, next, jumper"
    :total="pagination.total"
    @size-change="handleSizeChange"
    @current-change="handleCurrentChange"
/&gt;
</code></pre>
<p>分页组件的数据模型需要三个属性:</p>
<pre><code class="language-ts">//分页参数接口
export interface PaginationParam {
currentPage: number,
pageSize: number,
total: number
}
</code></pre>
<p>currentPage和pageSize需要指定默认值,而total是在后端传递过来的:</p>
<pre><code class="language-vue">&lt;!-- 分页组件Pagination --&gt;
&lt;el-pagination
    v-model:current-page="pagination.currentPage"
    v-model:page-size="pagination.pageSize"
    :page-sizes=""
    layout="total, sizes, prev, pager, next, jumper"
    :total="pagination.total"
    @size-change="handleSizeChange"
    @current-change="handleCurrentChange"
/&gt;
</code></pre>
<pre><code class="language-ts">//分页条组件的数据模型
let pagination = ref&lt;PaginationParam&gt;({
    //指定默认值
        currentPage : 1,
        pageSize : 5,
        total : 0
});
</code></pre>
<p>分页组件的current-page和page-size是v-model双向数据绑定,在用户点击的时候自定变为用户点击的值,并且触发@size-change和@current-change事件</p>
<h4 id="页面交互">页面交互</h4>
<h5 id="分页查询功能">分页查询功能</h5>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225150236.png"></p>
<p>需要的数据模型:</p>
<p>根据接口文档可以定义请求参数的数据模型:</p>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225145907.png"></p>
<p>而我们在上文中定义了两个数据模型:</p>
<pre><code class="language-ts">//分页数据模型
export interface PaginationParam {
currentPage: number,
pageSize: number,
total: number
}

//搜索栏数据模型
export interface SearchEmpModel {
name: string, //姓名
gender: string, //性别
begin: string, //开始时间
end: string, //结束时间
date: string[] //时间范围
}

//继承这两个数据模型
export interface EmpPageQueryParam extends SearchEmpModel,PaginationParam{

}
</code></pre>
<p>根据接口文档可以定义响应数据的数据模型:</p>
<pre><code class="language-json">//响应的数据:
{
"code": 1,
"msg": "success",
"data": {
    "total": 1,
    "rows": [
       {
      "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"
      }
]
}
</code></pre>
<p>数据模型:</p>
<pre><code class="language-ts">//分页结果接口
export interface PageModel {
total: number,
rows: any[]
}

//统一响应结果接口
export interface PageResultModel {
code: number,
msg: string,
data: PageModel
}
</code></pre>
<p>或者可以定义为:</p>
<pre><code class="language-ts">export interface ResultModel&lt;T&gt; {
    code: number,
    msg: string,
    data: T
}
export interface PageModel {
    total: number,
    rows: any[]
}
</code></pre>
<p>提高了复用性</p>
<p>API接口层:</p>
<pre><code class="language-ts">export const pageQueryApi =
    (param:EmpPageQueryParam) =&gt; request.get&lt;any,PageResultModel&gt;
(`/emps?name=${param.name}&amp;gender=${param.gender}&amp;begin=${param.begin}
                              &amp;end=${param.end}&amp;page=${param.currentPage}&amp;pageSize=${param.pageSize}`)
</code></pre>
<p>或者是:</p>
<pre><code class="language-ts">export const myPageQueryApi =
    (param:EmpPageQueryParam) =&gt; request.get&lt;any,ResultModel&lt;PageModel&gt;&gt;
(`/emps?name=${param.name}&amp;gender=${param.gender}&amp;begin=${param.begin}&amp;end=${param.end}
&amp;page=${param.currentPage}&amp;pageSize=${param.pageSize}`)
</code></pre>
<ul>
<li>查询:</li>
</ul>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225202935.png"></p>
<pre><code class="language-ts">let search = async () =&gt; {

let pageBean = await pageQueryApi({...searchEmp.value, ...pagination.value});

//更新列表
empList.value = pageBean.data.rows;
//更新记录条数
pagination.value.total = pageBean.data.total;
}
</code></pre>
<blockquote>
<p>页码、条数变化的时候也需要调用search</p>
</blockquote>
<h5 id="清空功能">清空功能</h5>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225202956.png"></p>
<pre><code class="language-ts">let clear = async ()=&gt; {
//清空搜索栏
searchEmp.value = {
    name: '',
    gender: '',
    begin: '',
    end: '',
    date: []
};
//再次查询
search();
}
</code></pre>
<p>在清空之后,以下属性都变为了空字符串:</p>
<pre><code class="language-ts">    name: '',
    gender: '',
    begin: '',
    end: '',
</code></pre>
<p>而我们在后端mybatis的动态SQL中对空字符串进行了判断。</p>
<ul>
<li>页面加载完成自动查询</li>
</ul>
<h3 id="新增员工">新增员工</h3>
<p>页面布局流程:</p>
<ul>
<li>确定要使用的Element组件</li>
<li>确定涉及到的数据模型</li>
</ul>
<h4 id="页面布局-3">页面布局</h4>
<p>点击按钮 弹出对话框,新增/编辑员工,需要的数据有两部分:员工信息和工作经历信息</p>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225203125.png"></p>
<p>涉及的数据模型:</p>
<pre><code class="language-ts">//员工工作经历数据接口
export interface EmpExprModel {
id?: number,
empId?: number,
exprDate: string[], //时间范围
begin: string,
end: string,
company: string,
job: string
}

//员工数据接口
export interface EmpModel {
id?: number,
username: string,
password: string,
name: string,
gender: string,
phone: string,
job: string,
salary: string,
image: string,
entryDate: string,
deptId: string,
deptName?: string,
exprList: EmpExprModel[]
}

</code></pre>
<p><strong>注意</strong>:数据模型中属性名的定义要参照接口文档</p>
<p>定义响应式对象:</p>
<pre><code class="language-ts">let formTitle = ref&lt;string&gt;('');
let dialogFormVisible = ref&lt;boolean&gt;(true);
let labelWidth = ref&lt;number&gt;(80);

let emp = ref&lt;EmpModel&gt;({
username : '',
password : '',
name : '',
gender : '',
phone: '',
job: '',
salary: '',
image: '',
entryDate: '',
deptId: '',
deptName: '',
exprList : []
});
</code></pre>
<h5 id="用户名姓名布局">用户名/姓名布局</h5>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225205449.png"></p>
<pre><code class="language-vue">&lt;el-dialog v-model="dialogFormVisible" :title="formTitle"&gt;
&lt;el-form :model="emp"&gt;    &lt;el-form-item label="用户名" :label-width="formLabelWidth"&gt;
      &lt;el-input v-model="emp.username" autocomplete="off" /&gt;    &lt;/el-form-item&gt;
    &lt;el-form-item label="姓名" :label-width="formLabelWidth"&gt;
      &lt;el-input v-model="emp.name" autocomplete="off" /&gt;    &lt;/el-form-item&gt;
    &lt;el-form-item label="性别" :label-width="formLabelWidth"&gt;
      &lt;el-select v-model="emp.gender" placeholder="请选择"&gt;
      &lt;el-option label="男" value="1" /&gt;
      &lt;el-option label="女" value="2" /&gt;
      &lt;/el-select&gt;   
    &lt;/el-form-item&gt;
&lt;/el-form&gt;
&lt;template #footer&gt;   
&lt;span class="dialog-footer"&gt;      
          &lt;el-button @click="dialogFormVisible = false"&gt;取消&lt;/el-button&gt;
          &lt;el-button type="primary" @click="dialogFormVisible = false"&gt;
                保存
          &lt;/el-button&gt;   
&lt;/span&gt;
&lt;/template&gt;
&lt;/el-dialog&gt;
</code></pre>
<p>当前显示的效果:</p>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225173427.png"></p>
<p>页面原型要求的显示效果:</p>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225202653.png"></p>
<p>要在一行中显示两个表单组件,就需要ElementPlus提供的Layout布局组件:通过基础的 24 分栏,迅速简便地创建布局。</p>
<p>Layout布局将一行(一个el-row)等分为24份,如果想设置两个组件大小相等,只需要分别设置两个组件(el-col)的属性 :span = 12</p>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225203437.png"></p>
<pre><code class="language-vue">&lt;el-dialog v-model="dialogFormVisible" :title="formTitle"&gt;
&lt;el-form :model="emp"&gt;

    &lt;el-row&gt;
      &lt;el-col :span="12"&gt;
      &lt;el-form-item label="用户名" :label-width="formLabelWidth"&gt;
          &lt;el-input v-model="emp.username" autocomplete="off" /&gt;
      &lt;/el-form-item&gt;
      &lt;/el-col&gt;
      &lt;el-col :span="12"&gt;
      &lt;el-form-item label="姓名" :label-width="formLabelWidth"&gt;
          &lt;el-input v-model="emp.name" autocomplete="off" /&gt;
      &lt;/el-form-item&gt;
      &lt;/el-col&gt;
    &lt;/el-row&gt;

    &lt;el-form-item label="性别" :label-width="formLabelWidth"&gt;
      &lt;el-select v-model="emp.gender" placeholder="请选择"&gt;
      &lt;el-option label="男" value="1" /&gt;
      &lt;el-option label="女" value="2" /&gt;
      &lt;/el-select&gt;
    &lt;/el-form-item&gt;
&lt;/el-form&gt;
&lt;template #footer&gt;
    &lt;span class="dialog-footer"&gt;
      &lt;el-button @click="dialogFormVisible = false"&gt;取消&lt;/el-button&gt;
      &lt;el-button type="primary" @click="dialogFormVisible = false"&gt;
      保存
      &lt;/el-button&gt;
    &lt;/span&gt;
&lt;/template&gt;
&lt;/el-dialog&gt;
</code></pre>
<h5 id="性别职位布局列表优化">性别/职位布局:列表优化</h5>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225205522.png"></p>
<p>之前的布局方式:</p>
<pre><code class="language-vue">&lt;el-form-item label="性别"&gt;
&lt;el-select v-model="searchEmp.gender" placeholder="请选择"&gt;
    &lt;el-option label="男" value="1"/&gt;
    &lt;el-option label="女" value="2"/&gt;
&lt;/el-select&gt;
&lt;/el-form-item&gt;
</code></pre>
<p>这样做是没有问题的,但是如果后续要求 "男" 变为 "男士",就要在HTML结构中一个一个修改,这样做太麻烦了。</p>
<p>建议做法:<strong>下拉列表的多个选项在数据模型中统一维护</strong>,好处是如果要添加选项/修改选项,就不需要在HTML中进行更改了</p>
<p>定义gender和job的响应式数据:</p>
<pre><code class="language-ts">const genders = ref([{name : '男', value : '1'},{name : '女', value : '2'}])

const jobs = ref([
{ name: '班主任', value: 1 },
{ name: '讲师', value: 2 },
{ name: '学工主管', value: 3 },
{ name: '教研主管', value: 4 },
{ name: '咨询师', value: 5 },
{ name: '其他', value: 6 }
])
</code></pre>
<p>在下拉列表中展示时:</p>
<pre><code class="language-vue">&lt;!--性别:第二行的第一列--&gt;
&lt;el-col :span="12"&gt;
&lt;el-form-item label="性别" :label-width="labelWidth"&gt;
    &lt;el-select v-model="emp.gender" placeholder="请选择" style="width: 100%;"&gt;       &lt;!--label属性:选项显示的内容需要动态绑定--&gt;
      &lt;el-option v-for="gender in genders" :key="gender.value" :value="gender.value" :label="gender.name"/&gt;
    &lt;/el-select&gt;
&lt;/el-form-item&gt;
&lt;/el-col&gt;

&lt;!--职位:第四行的第二列--&gt;
&lt;el-col :span="12"&gt;
&lt;el-form-item label="职位" :label-width="labelWidth"&gt;
    &lt;el-select v-model="emp.job" placeholder="请选择" style="width: 100%;"&gt;
      &lt;el-option v-for="job in jobs" :key="job.value" :label="job.name" :value="job.value" /&gt;
    &lt;/el-select&gt;
&lt;/el-form-item&gt;
&lt;/el-col&gt;
</code></pre>
<h5 id="部门布局">部门布局</h5>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225210805.png"></p>
<p>与上文中jobs、genders不同的是,部门数据应该是在后端查询后返回的,在api/dept.ts定义了查询所有部门的方法:</p>
<pre><code class="language-ts">//dept.ts
export const queryAllApi = () =&gt; request.get&lt;any,ResultModel&gt;('/depts');
</code></pre>
<p>我们需要引入这个方法,但是引入这个方法名:queryAllApi 可能与本文件中其他的方法名冲突,可以指定别名:</p>
<pre><code class="language-ts">import {queryAllApi as queryAllDeptsApi} from '@/api/dept'

let depts = ref&lt;DeptModelArray&gt;([]);
const queryAllDepts = async ()=&gt; {   
let result = await queryAllDeptsApi();
depts.value = result.data;
}
</code></pre>
<ul>
<li>queryAllDepts方法应该何时调用?</li>
</ul>
<p>点击编辑和新增都会使用到这个对话框,也就是都需要使用部门数据,应该放在EmpIndexView的onMounted方法中调用:</p>
<pre><code class="language-ts">onMounted(() =&gt; {
search();
queryAllDepts();
})
</code></pre>
<p>此时所有的信息都被封装在depts中了,在下拉列表中渲染选项:</p>
<pre><code class="language-vue">&lt;el-col :span="12"&gt;
&lt;el-form-item label="所属部门" :label-width="labelWidth"&gt;
    &lt;el-select v-model="emp.deptId" placeholder="请选择" style="width: 100%;"&gt;
      &lt;el-option v-for="dept in depts" :key="dept.id" :label="dept.name" :value="dept.id" /&gt;&lt;!--value指定为id--&gt;
    &lt;/el-select&gt;
&lt;/el-form-item&gt;
&lt;/el-col&gt;
</code></pre>
<p>value属性是最终提交的值,需要指定为id</p>
<h5 id="头像布局">头像布局</h5>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225213258.png"></p>
<pre><code class="language-vue">&lt;template&gt;
&lt;el-upload
    class="avatar-uploader"
    action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
    :show-file-list="false"
    :on-success="handleAvatarSuccess"
    :before-upload="beforeAvatarUpload"
&gt;
&lt;!--
    action:上传地址
        on-success:上传成功hook
        before-upload:上传之前的hook
--&gt;
    &lt;img v-if="imageUrl" :src="imageUrl" class="avatar" /&gt;
    &lt;el-icon v-else class="avatar-uploader-icon"&gt;&lt;Plus /&gt;&lt;/el-icon&gt;
&lt;/el-upload&gt;
&lt;/template&gt;

&lt;script lang="ts" setup&gt;
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'

import type { UploadProps } from 'element-plus'

const imageUrl = ref('')

//成功上传的回调函数
const handleAvatarSuccess: UploadProps['onSuccess'] = (
response,
uploadFile
) =&gt; {
imageUrl.value = URL.createObjectURL(uploadFile.raw!)
}

//上传之前的回调函数,返回false不进行上传,返回true进行上传
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) =&gt; {
if (rawFile.type !== 'image/jpeg') {
    ElMessage.error('Avatar picture must be JPG format!')
    return false
} else if (rawFile.size / 1024 / 1024 &gt; 2) {
    ElMessage.error('Avatar picture size can not exceed 2MB!')
    return false
}
return true
}
&lt;/script&gt;
</code></pre>
<ul>
<li>before-upload:上传之前的回调函数,<strong>一般在该函数中进行文件校验</strong></li>
<li>on-success:在该函数中写回URL路径</li>
</ul>
<p>上传的效果:点击Icon上传,上传成功后显示上传的图片,核心的逻辑就是以下代码控制的:</p>
<pre><code class="language-vue">    &lt;img v-if="imageUrl" :src="imageUrl" class="avatar" /&gt;
    &lt;el-icon v-else class="avatar-uploader-icon"&gt;&lt;Plus /&gt;&lt;/el-icon&gt;
</code></pre>
<p>未上传时URL是空值,v-if不渲染img,渲染上传的Icon Plus,上传成功后,handleAvatarSuccess回调函数会将URL写入imageUrl,v-if渲染img,不渲染Icon</p>
<p><strong>上传的核心属性:action</strong>,对于本系统的后端接口/upload来说:</p>
<pre><code class="language-vue">&lt;el-upload
    class="avatar-uploader"
    action="/upload"
    :show-file-list="false"
    :on-success="handleAvatarSuccess"
    :before-upload="beforeAvatarUpload"
&gt;
</code></pre>
<p>这样是无法访问到我们的接口的,因为请求路径是:http://127.0.0.1:5173/upload</p>
<p>这个请求不是经过axios发送的,是el-upload组件发送的,不会加上/api路径,如果想让服务器进行跨域代理,需要设置action为:/api/upload</p>
<pre><code class="language-vue">&lt;!-- 第五行 --&gt;
&lt;el-row&gt;
&lt;el-col :span="12"&gt;
    &lt;el-form-item label="头像":label-width="labelWidth"&gt;
      &lt;el-upload
          class="avatar-uploader"
          action="/api/upload"
          :show-file-list="false"
          :on-success="handleAvatarSuccess"
          :before-upload="beforeAvatarUpload"
      &gt;
      &lt;img v-if="emp.image" :src="emp.image" class="avatar" /&gt; &lt;!--有url就显示图片--&gt;
      &lt;el-icon v-else class="avatar-uploader-icon"&gt;&lt;Plus /&gt;&lt;/el-icon&gt;&lt;!--没有url就显示图标--&gt;
      &lt;/el-upload&gt;
    &lt;/el-form-item&gt;
&lt;/el-col&gt;
&lt;/el-row&gt;
</code></pre>
<h5 id="工作经历布局">工作经历布局</h5>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231225215213.png"></p>
<p>点击添加工作经历,工作经历表单多一条记录;点击删除按钮,删除对应的记录</p>
<p>这个功能看起来比较复杂,需要谨记Vue的原则:<font size="15px" color="red">Vue是基于数据驱动视图展示的</font></p>
<p>数据改变引起了视图的改变,对于工作经历来说,这个数组是具有响应式的:</p>
<ul>
<li>添加时,向数组里添加元素</li>
<li>删除时,删除数组里的元素</li>
</ul>
<p><strong>一旦数据发生变化,视图中展示的数据就会发生变化</strong></p>
<p>布局:</p>
<pre><code class="language-vue">&lt;!-- 第六行 --&gt;
&lt;el-row&gt;
&lt;el-col :span="24"&gt;
    &lt;el-form-item label="工作经历" :label-width="labelWidth"&gt;
      &lt;el-button type="success" size="small" @click="addEmpExpr"&gt;+ 添加工作经历&lt;/el-button&gt;
    &lt;/el-form-item&gt;
&lt;/el-col&gt;
&lt;/el-row&gt;

&lt;!-- 遍历emp.exprList数组,渲染每一条工作经历 --&gt;
&lt;el-row v-for="(expr,index) in emp.exprList" :gutter="5"&gt;
&lt;el-col :span="10"&gt;
    &lt;el-form-item label="时间" size="small" :label-width="labelWidth"&gt;
      &lt;el-date-picker
              v-model="expr.exprDate"
              type="daterange"
              range-separator="至"
              start-placeholder="开始时间"
              end-placeholder="结束时间"
              value-format="YYYY-MM-DD"
          /&gt;
    &lt;/el-form-item&gt;
&lt;/el-col&gt;

&lt;el-col :span="6"&gt;
    &lt;el-form-item label="公司" size="small"&gt;
      &lt;el-input placeholder="公司名称" v-model="expr.company" /&gt;
    &lt;/el-form-item&gt;
&lt;/el-col&gt;

&lt;el-col :span="6"&gt;
    &lt;el-form-item label="职位" size="small"&gt;
      &lt;el-input placeholder="职位名称" v-model="expr.job" /&gt;
    &lt;/el-form-item&gt;
&lt;/el-col&gt;

&lt;el-col :span="2"&gt;
    &lt;el-form-item size="small"&gt;
      &lt;el-button type="danger" @click="del(index/expr)"&gt;- 删除&lt;/el-button&gt;
    &lt;/el-form-item&gt;
&lt;/el-col&gt;
&lt;/el-row&gt;
</code></pre>
<p>函数:</p>
<pre><code class="language-ts">//添加工作经历的函数
const addEmpExpr = ()=&gt; {
        emp.value.exprList.push({exprDate : [],begin : '',end : '',company : '', job : ''})
}

//删除

//根据索引删除
const del = (index:number)=&gt; {
        emp.value.exprList.splice(index,0,1);
}

/*
严格模式下不能使用
const del = (expr:EmpExprModel)=&gt; {
with (emp.value.exprList) {
    splice(indexOf(expr),1);
}
}
*/

//根据对象删除
const del = (expr:EmpExprModel)=&gt; {
        let index = emp.value.exprList.indexOf(expr);
        emp.value.exprList.splice(index,0,1);
}
</code></pre>
<p>接口文档要求的请求参数:</p>
<pre><code class="language-json">{
"image": "https://web-framework.oss-cn-hangzhou.aliyuncs.com/2022-09-03-07-37-38222.jpg",
"username": "linpingzhi",
"name": "林平之",
"gender": 1,
"job": 1,
"entrydate": "2022-09-18",
"deptId": 1,
"phone": "18809091234",
"salary": 8000,
"exprList": [
      {
         "company": "百度科技股份有限公司",
         "job": "java开发",
         "begin": "2012-07-01",
         "end": "2019-03-03"
      },
      {
         "company": "阿里巴巴科技股份有限公司",
         "job": "架构师",
         "begin": "2019-03-15",
         "end": "2023-03-01"
      }
   ]
}
</code></pre>
<p>我们当前的EmpExpr数据模型:</p>
<pre><code class="language-ts">export interface EmpExprModel {
id?: number,
empId?: number,
exprDate: string[], //时间范围
begin: string,
end: string,
company: string,
job: string
}
</code></pre>
<p><strong>就需要对emp.value.exprList进行操作,将每一条数据的exprDate转变为end和begin</strong></p>
<pre><code class="language-ts">watch(emp,(newVal,oldVal) =&gt; {
if (emp.value.exprList.length &gt; 0){
    emp.value.exprList.forEach(expr =&gt; {
      expr.end = expr.exprDate;
      expr.begin = expr.exprDate;
    })
}
},{deep : true})
</code></pre>
<h4 id="页面交互-1">页面交互</h4>
<p>完成新增员工的功能</p>
<p>为新增员工按钮绑定事件:</p>
<pre><code class="language-ts">const addEmp = ()=&gt; {
//清空上一次的表单数据
emp.value = {
    username : '',
    password : '',
    name : '',
    gender : '',
    phone: '',
    job: '',
    salary: '',
    image: '',
    entryDate: '',
    deptId: '',
    deptName: '',
    exprList : []
}
   
dialogFormVisible.value = true;
}
</code></pre>
<p>打开对话框,为保存按钮添加事件</p>
<p>接口层:</p>
<pre><code class="language-ts">export const createEmpApi = (emp:EmpModel) =&gt; request.post&lt;any,ResultModel&gt;('/emps',emp);
</code></pre>
<p>调用:</p>
<pre><code class="language-ts">const save = async ()=&gt; {
//一定注意传递的入参是emp.value
let result = await createEmpApi(emp.value);
if (result.code){
    ElMessage.success('保存成功');
    dialogFormVisible.value = false;

    //重新查询
    search();
}else {
    ElMessage.error(result.msg);
}
}
</code></pre>
<h6 id="表单校验-1">表单校验</h6>
<p>在提交之前还需要进行<strong>表单校验</strong></p>
<p>对新增员工进行表单校验需要参照界面原型的要求:</p>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231226085744.png"></p>
<p>总结出如下的校验规则:</p>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231226085925.png"></p>
<p>表单校验的流程:</p>
<ol>
<li>定义表单实例 empFormRef,赋值给ref属性,用来在save方法中校验表单和在openDialog方法中重置表单状态</li>
<li>定义校验规则 FormRules,其中的泛型指定表单对应的数据模型,在需要校验的表单项上通过prop指定规则名称</li>
</ol>
<pre><code class="language-vue">&lt;el-form :model="emp" ref="empFormRef" :rules="rules"&gt;
&lt;el-form-item prop='校验规则名称'&gt;
</code></pre>
<p>表单验证时机:</p>
<ol>
<li>保存(新增/编辑)时,校验通过提交数据,不通过提示信息</li>
<li>打开对话框(新增/修改)时,重置表单校验规则</li>
</ol>
<p>验证时机:</p>
<pre><code class="language-ts">const save = async ()=&gt; {
//注意async的位置
await empFormRef.value?.validate(async (valid,fields) =&gt; {
    if (valid){
      //一定注意传递的入参是emp.value
      let result = await createEmpApi(emp.value);
      if (result.code){
      ElMessage.success('保存成功');
      dialogFormVisible.value = false;
      search();
      }else {
      ElMessage.error(result.msg);
      }
    }else {
      ElMessage.error('表单校验失败,不能提交');
    }
})
}
</code></pre>
<p>重置表单校验规则:</p>
<pre><code class="language-ts">const openForm = ()=&gt; {
emp.value = {
    username : '',
    password : '',
    name : '',
    gender : '',
    phone: '',
    job: '',
    salary: '',
    image: '',
    entryDate: '',
    deptId: '',
    deptName: '',
    exprList : []
};
//重置表单
empFormRef.value?.resetFields();
dialogFormVisible.value = true;
}

//新增员工按钮
const addEmp = ()=&gt; {
formTitle.value = '新增员工';
openForm();
}

//编辑员工按钮
const update = async (id:number) =&gt; {
formTitle.value = '编辑员工';
openForm();

let result = await queryByIdApi(id);
if (result.code){
    emp.value = result.data;
    //后端会返回exprList,不需要判断空
    emp.value.exprList.forEach(expr =&gt; {
      expr.exprDate = ;
    })
}else {
    ElMessage.error('查询失败')
}
}
</code></pre>
<h3 id="修改员工">修改员工</h3>
<ol>
<li>点击编辑按钮,数据回显:根据ID查询员工信息</li>
<li>点击保存,执行修改操作</li>
</ol>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231226095314.png"></p>
<h4 id="数据回显">数据回显</h4>
<p>接口层:</p>
<pre><code class="language-ts">export const queryByIdApi = (id:number) =&gt; request.get&lt;any,ResultModel&gt;(`/emps/${id}`)
</code></pre>
<p>更新方法:</p>
<pre><code class="language-ts">const update = async (id:number) =&gt; {
formTitle.value = '编辑员工';
openForm();
let result = await queryByIdApi(id);

if (result.code){
    emp.value = result.data;
    //后端会返回exprList,不需要判断空
    emp.value.exprList.forEach(expr =&gt; {
      expr.exprDate = ;
    })
}else {
    ElMessage.error('查询失败')
}
}
</code></pre>
<p>但是这样做是有问题的,数据回显不能显示。</p>
<p>之前的watch监听器将工作经历的exprDate转化为begin和end的代码:</p>
<pre><code class="language-ts">watch(emp,(newVal,oldVal) =&gt; {
if (emp.value.exprList.length &gt; 0){
    emp.value.exprList.map(expr =&gt; {
      expr.end = expr.exprDate;
      expr.begin = expr.exprDate;
    })
}
},{deep : true})
</code></pre>
<p>只要emp发生变化,就对emp.value.exprList进行遍历,遍历时将exprDate数组分别赋值给begin、end</p>
<p>emp变化的三种清空:</p>
<ul>
<li>新增员工时发生变化,exprList可能是空数组,不会进行map,但最好判断exprList的长度 > 0</li>
<li>清空emp时发生变化,exprList是空数组,不进行map</li>
<li>数据回显时发生变化,exprList不是空数组,进行map,访问exprDate数组的元素</li>
</ul>
<p>但是在数据回显的时候,<strong>后端接口没有返回exprDate属性</strong>,此时就是访问了undefined的元素,就会报错。</p>
<p>所以需要对watch再加一次判断,在exprDate不为空的时候进行赋值:</p>
<pre><code class="language-ts">watch(() =&gt; emp.value.exprList,(newVal,oldVal) =&gt; {
if (emp.value.exprList.length &gt; 0){
    emp.value.exprList.map(expr =&gt; {
      if (!expr.exprDate){
      return;
      }
      expr.end = expr.exprDate;
      expr.begin = expr.exprDate;
    })
}
},{deep : true})
</code></pre>
<p>这样就避免了在数据回显时导致emp发生变化触发此监听器,从而导致访问undefined。</p>
<h4 id="保存修改-1">保存修改</h4>
<p>和新增员工使用同一个对话框,form表单的保存按钮绑定的是一个方法:</p>
<pre><code class="language-vue">&lt;!--保存/取消--&gt;
&lt;template #footer&gt;
&lt;span class="dialog-footer"&gt;
    &lt;el-button @click="dialogFormVisible = false"&gt;取消&lt;/el-button&gt;
    &lt;el-button type="primary" @click="save"&gt;保存&lt;/el-button&gt;
&lt;/span&gt;
&lt;/template&gt;
</code></pre>
<p>新增员工时的保存方法:</p>
<pre><code class="language-ts">const save = async ()=&gt; {
//注意async的位置
await empFormRef.value?.validate(async (valid,fields) =&gt; {
    if (valid){
      //一定注意传递的入参是emp.value
      let result = await createEmpApi(emp.value);
      if (result.code){
      ElMessage.success('保存成功');
      dialogFormVisible.value = false;
      search();
      }else {
      ElMessage.error(result.msg);
      }
    }else {
      ElMessage.error('表单校验失败,不能提交');
    }
})
}
</code></pre>
<p>新增和修改的区别就是新增是没有id的,修改有id,所以可以根据有无id的区别来调用新增和修改的不同接口层方法:</p>
<pre><code class="language-ts">const save = async ()=&gt; {
//注意async的位置
await empFormRef.value?.validate(async (valid,fields) =&gt; {
    if (valid){
      let result;

      if (!emp.value.id){
      //无id新增
      result = await createEmpApi(emp.value);
      }else {
      //有id修改
      result = await modifyEmpApi(emp.value);
      }
      
      if (result.code){
      ElMessage.success('保存成功');
      dialogFormVisible.value = false;
      search();
      }else {
      ElMessage.error(result.msg);
      }
    }else {
      ElMessage.error('表单校验失败,不能提交');
    }
})
}
</code></pre>
<h3 id="删除员工">删除员工</h3>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231226095619.png"></p>
<p>删除员工信息有两个操作入口:</p>
<ul>
<li>点击每条记录之后的 删除 按钮,删除当前条记录。</li>
<li>点击多选框选中要删除的员工,点击批量删除,批量删除员工信息</li>
</ul>
<blockquote>
<p>批量删除或删除最终只需要调用服务端的同一个批量删除接口即可。</p>
</blockquote>
<p>接口文档:</p>
<pre><code class="language-bash">/emps?ids=1,2,3
</code></pre>
<p>删除的数据以get默认形式传递,接口层:</p>
<pre><code class="language-ts">export const deleteApi = (ids:number[]) =&gt; request.delete&lt;any,ResultModel&gt;(`/emp/${ids}`)
</code></pre>
<p>以number[] 作为路径参数会自动将数组元素转化为 /emp/1,2,3</p>
<p><strong>单个删除</strong>:点击删除按钮,删除单个数据</p>
<pre><code class="language-ts">const deleteById = (id:number) =&gt; {
ElMessageBox.confirm(
      '是否确认删除?',
      'Warning',
      {
      confirmButtonText: '确认',
      cancelButtonText: '取消',
      type: 'warning',
      }
)   //注意async的位置
      .then(async () =&gt; {
      //确认删除的回调函数
          //接口层入参是数组形式
      let result = await deleteApi();
      if (result.code){
          //删除成功
          ElMessage.success('删除成功')

          search();
      }else{
          //删除失败:展示服务器响应的信息
          ElMessage.error(result.msg)
      }
      })
      .catch(() =&gt; {
      ElMessage({
          type: 'info',
          message: '取消删除',
      })
      })
}
</code></pre>
<ul>
<li>批量删除</li>
</ul>
<blockquote>
<p>多选框的实现参照ElementPlus官网:实现多选非常简单,手动添加一个 <code>el-table-column</code>,设 <code>type</code> 属性为 <code>selection</code> 即可。</p>
</blockquote>
<p><strong>多选框选项发生变化时会发生change事件</strong>,在ElementPlus中通过属性@selection-change指定回调函数:</p>
<pre><code class="language-vue">&lt;el-table
    :data="empList"
    border
    style="width: 100%"
    fit
    @selection-change="handleSelectionChange"
&gt;
&lt;!--多选框--&gt;
&lt;el-table-column type="selection" width="55" /&gt;
&lt;el-table-column prop="name" label="姓名" align="center" width="130px" /&gt;
&lt;el-table-column prop="gender" label="性别" align="center" width="100px"/&gt;
&lt;el-table-column prop="image" label="头像" align="center"/&gt;
&lt;el-table-column prop="deptName" label="所属部门" align="center" /&gt;
&lt;el-table-column prop="job" label="职位" align="center" width="100px"/&gt;
&lt;el-table-column prop="entryDate" label="入职时间" align="center" width="130px" /&gt;
&lt;el-table-column prop="updateTime" label="最后修改时间" align="center" /&gt;
&lt;el-table-column label="操作" align="center"&gt;
    &lt;template #default="scope"&gt;
      &lt;el-button type="primary" size="small" @click="update(scope.row.id)"&gt;编辑&lt;/el-button&gt;
      &lt;el-button type="danger" size="small" @click="deleteById(scope.row.id)"&gt;删除&lt;/el-button&gt;
    &lt;/template&gt;
&lt;/el-table-column&gt;
&lt;/el-table&gt;
</code></pre>
<p><strong>回调函数应该将所有选中项的id存储在数组中</strong></p>
<pre><code class="language-ts">let ids = ref&lt;(number|undefined)[]&gt;([]);

//多选框选择项变化
const handleSelectionChange = (selectedEmps:EmpModelArray)=&gt; {
//每次选中元素都会触发该方法
ids.value = selectedEmps.map(e =&gt; e.id);
}
</code></pre>
<p>批量删除的方法和单个删除的方法只有一个地方不同:</p>
<pre><code class="language-ts">//单个删除
let result = await deleteApi();

//批量删除
let result = await deleteApi(ids.value);
</code></pre>
<p>可以抽取为deleteEmpBatch方法:</p>
<pre><code class="language-ts">const deleteEmpBatch = (id?:number) =&gt; {
ElMessageBox.confirm(
      '是否确认删除?',
      'Warning',
      {
      confirmButtonText: '确认',
      cancelButtonText: '取消',
      type: 'warning',
      }
)   
      .then(async () =&gt; {
      let result;

      if (id){//传递了入参id就单个删除
          result = await deleteApi();
      }else {//否则就多个删除
          //接口层为字符串入参:result = await deleteApi(ids.value.join(','))
          result = await deleteApi(ids.value)
      }

      if (result.code){
          //删除成功
          ElMessage.success('删除成功')

          search();
      }else{
          //删除失败:展示服务器响应的信息
          ElMessage.error(result.msg)
      }
      })
      .catch(() =&gt; {
      ElMessage({
          type: 'info',
          message: '取消删除',
      })
      })
}
</code></pre>
<p>但是这样做是有问题的,在此处只判断id是否存在的话,如果id不存在会将事件对象event传递进来,所以还需要判断id是否为number类型的</p>
<p>可以通过三目运算符简化:</p>
<pre><code class="language-ts">const deleteEmpBatch = (id?:number) =&gt; {
ElMessageBox.confirm(
      '是否确认删除?',
      'Warning',
      {
      confirmButtonText: '确认',
      cancelButtonText: '取消',
      type: 'warning',
      }
)   
      .then(async () =&gt; {

      let result;

      result = await deleteApi(id &amp;&amp; type of id === 'number' ? : ids.value)

      if (result.code){
          //删除成功
          ElMessage.success('删除成功')
          search();
      }else{
          //删除失败:展示服务器响应的信息
          ElMessage.error(result.msg)
      }
      })
      .catch(() =&gt; {
      ElMessage({
          type: 'info',
          message: '取消删除',
      })
      })
}
</code></pre>
<blockquote>
<p>如果接口层的入参是string类型,需要传递:ids.value.join(',')</p>
</blockquote>
<p>绑定事件:</p>
<pre><code class="language-vue">&lt;el-button type="danger" @click="deleteEmpBatch"&gt;- 批量删除&lt;/el-button&gt;

&lt;el-button type="danger" size="small" @click="deleteEmpBatch(scope.row.id)"&gt;删除&lt;/el-button&gt;
</code></pre>
<h2 id="登录">登录</h2>
<h3 id="页面布局-4">页面布局</h3>
<pre><code class="language-vue">&lt;script setup lang="ts"&gt;
import { ref } from 'vue'
import type { LoginEmp } from '@/api/model/model'
let loginForm = ref&lt;LoginEmp&gt;({username:'', password:''})

&lt;/script&gt;

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

      &lt;el-form-item label="密码" prop="password"&gt;
          &lt;el-input type="password" v-model="loginForm.password" placeholder="请输入密码"&gt;&lt;/el-input&gt;
      &lt;/el-form-item&gt;

      &lt;el-form-item&gt;
          &lt;el-button class="button" type="primary" @click=""&gt;登 录&lt;/el-button&gt;
          &lt;el-button class="button" type="info" @click=""&gt;重 置&lt;/el-button&gt;
      &lt;/el-form-item&gt;
      &lt;/el-form&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;/template&gt;

&lt;style scoped&gt;
#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;
}
&lt;/style&gt;
</code></pre>
<h3 id="页面交互-2">页面交互</h3>
<p>用户登录成功后跳转到主页面,并且以后每次请求都要携带token</p>
<ul>
<li>完成基本的员工登录操作</li>
</ul>
<pre><code class="language-ts">import { ref } from 'vue'
import type { LoginEmp } from '@/api/model/model'
import {loginApi} from "@/api/login";
import {ElMessage} from "element-plus";
import {useRouter} from "vue-router";

let loginForm = ref&lt;LoginEmp&gt;({username:'', password:''})
//获取当前应用的路由实例
let router = useRouter();

const login = async () =&gt; {
let result = await loginApi(loginForm.value);
if (result.code){
    ElMessage.success('登录成功');
        //1. 存储token
          
        //2. 跳转页面
        router.push('/index');
    }else {
    ElMessage.error('登录失败')
}
}
</code></pre>
<ul>
<li>将登陆成功后获取到的登录信息存储起来,方便在其他组件中使用</li>
</ul>
<p>如果在项目的多个组件中共享数据,可以使用Vue3提供的[]</p>
<p>在pinia中保存用户的登录信息:</p>
<pre><code class="language-ts">//loginEmp.ts
export const useLoginEmpStore = defineStore('loginEmp', () =&gt; {

//登录信息
const loginEmp = ref&lt;LoginInfo&gt;({});

//设置登录信息
const setLoginEmp = (loginEmpInfo:LoginInfo) =&gt; {
    loginEmp.value = loginEmpInfo;
}

//获取登录信息
const getLoginEmp = () =&gt; {
    return loginEmp.value;
}

//删除登录信息
const delLoginEmp = () =&gt; {
    loginEmp.value = {}
}

return { loginEmp, setLoginEmp, getLoginEmp,delLoginEmp }
})
</code></pre>
<blockquote>
<p>建议使用use + 名字 + Store的形式</p>
</blockquote>
<pre><code class="language-ts">const login = async () =&gt; {
let result = await loginApi(loginForm.value);
if (result.code){
    ElMessage.success('登录成功');
    //1. 存储token
    let loginEmpStore = useLoginEmpStore();
    loginEmpStore.setLoginEmp(result.data);
      
    //2. 跳转页面
    router.push('/index');

}else {
    ElMessage.error('登录失败')
}
}
</code></pre>
<p>token已经被存储在pinia中了,只需要在后续的请求中携带pinia中的token就可以。</p>
<p>现在的问题是如何在请求头中携带token,我们将所有的交互逻辑抽取到api层了:</p>
<pre><code class="language-ts">export const queryAllApi = () =&gt; request.get&lt;any,ResultModel&gt;('/depts');

//接口文档指明参数为dept类型
export const addApi = (dept:DeptModel) =&gt; request.post&lt;any,ResultModel&gt;('/depts',dept);

export const getInfoByIdApi = (id:number) =&gt; request.get&lt;any,ResultModel&gt;(`/depts/${id}`);

export const modifyInfoApi = (dept:DeptModel) =&gt; request.put&lt;any,ResultModel&gt;('/depts',dept);

export const removeByIdApi = (id:number) =&gt; request.delete&lt;any,ResultModel&gt;(`/depts?id=${id}`)
</code></pre>
<p>在请求时调用的是我们封装的request:</p>
<pre><code class="language-ts">import axios from 'axios'

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

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

export default request
</code></pre>
<p>在之前设置了响应拦截器,将AxiosResponse替换为服务器端响应的数据,<strong>也可以定义一个请求拦截器</strong>,为所有请求添加请求头token:</p>
<pre><code class="language-ts">import axios from 'axios'
import {useLoginEmpStore} from "@/stores/loginEmp";

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

request.interceptors.request.use((config) =&gt; {
      let loginEmpStore = useLoginEmpStore();
      let loginEmp = loginEmpStore.getLoginEmp();
         
      //如果登录信息存在并且有token
      if (loginEmp &amp;&amp; loginEmp.token){
          config.headers['token'] = loginEmp.token;
      }
      return config;
      
    }, (error) =&gt; {
      return Promise.reject(error);
    }
)

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

export default request
</code></pre>
<p>这样所有的请求都会携带token(如果用户的登录信息存在的话)</p>
<ul>
<li>如果用户没有登录,直接访问组件的路径,比如/index,<strong>服务器会响应401状态码,此时应该让页面跳转到登录界面</strong></li>
</ul>
<p><strong>第一种拦截方式:响应拦截器进行拦截</strong></p>
<p>在响应拦截器中进行统一的拦截,如果是401状态码就跳转到登录界面:</p>
<pre><code class="language-ts">//axios的响应 response 拦截器
request.interceptors.response.use(
    (response) =&gt; { //成功回调
      return response.data
    },
    (error) =&gt; {   
      //非2xx状态码会进入次回调
      //error是AxiosError对象,封装了response和request
      if (error.response.status == 401){
            ElMessage.error('登录失效,请重新登录');
            router.push('/login');
      }else {
            ElMessage.error('接口访问异常');//访问失败给用户提示
      }
      return Promise.reject(error)
    }
)
</code></pre>
<p>注意:此处不能使用useRouter()函数获取router对象,需要导入router对象:</p>
<pre><code class="language-ts">//index.ts
import { createRouter, createWebHistory } from 'vue-router'
import {useLoginEmpStore} from "@/stores/loginEmp";
import {ElMessage} from "element-plus";

const router = createRouter({
...
})

export default router
</code></pre>
<p>在router/index.ts中导出了router对象,其他地方使用也可以导入这个对象:</p>
<pre><code class="language-ts">//request.ts
import axios from 'axios'
import {useLoginEmpStore} from "@/stores/loginEmp";
import {ElMessage} from "element-plus";
import router from "@/router";   //导入了 @/router/index.ts,index.ts可以省略

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

request.interceptors.request.use((config) =&gt; {
        ...
);

//axios的响应 response 拦截器
request.interceptors.response.use(
        ...
)

export default request
</code></pre>
<p><strong>第二种拦截方式:全局前置路由守卫</strong></p>
<pre><code class="language-ts">//router/index.ts

router.beforeEach((to, from, next) =&gt; {

        //不是跳转到登录页面的路由都需要判断是否登录
    if (!to.path.match('/login')){

    let loginEmpStore = useLoginEmpStore();
    let loginEmp = loginEmpStore.getLoginEmp();
    if (loginEmp &amp;&amp; loginEmp.token){
      //登录后继续路由跳转
      next();
    }else{
      ElMessage.error('请先登录');
      //未登录跳转到登录界面
      router.push('/login');
    }

}else {
        //去往登录页面的路由直接跳转
    next();
}

})
</code></pre>
<p>相比之下,第二种路由跳转方式不会向服务器端发起请求,但是实际开发中两种方式往往结合使用</p>
<h2 id="退出登录">退出登录</h2>
<p>点击退出登录按钮,清空员工的登录信息,跳转到登录页面</p>
<pre><code class="language-vue">&lt;script setup lang="ts"&gt;
import {useLoginEmpStore} from "@/stores/loginEmp";
import router from "@/router";
import {ElMessage} from "element-plus";
import {ref} from "vue";

let loginEmpStore = useLoginEmpStore();
let name = ref&lt;string&gt;(loginEmpStore.getLoginEmp().name);


const logout = () =&gt; {
    //1. 清空登录信息
    loginEmpStore.delLoginEmp();
    //2. 跳转到登录界面
    ElMessage.success(`退出登录成功,${name.value}`);
    router.push('/login');
}
&lt;/script&gt;

&lt;template&gt;
&lt;span class="title"&gt;Tlias智能学习辅助系统&lt;/span&gt;
   
&lt;span class="right_tool"&gt;
          &lt;a href=""&gt;
            &lt;el-icon&gt;&lt;EditPen /&gt;&lt;/el-icon&gt; 修改密码 &amp;nbsp;&amp;nbsp;&amp;nbsp; |&amp;nbsp;&amp;nbsp;&amp;nbsp;
          &lt;/a&gt;
          &lt;!--让超链接失效--&gt;
          &lt;a href="javascript:void(0)" @click="logout"&gt;   
            &lt;el-icon&gt;&lt;SwitchButton /&gt;&lt;/el-icon&gt; 退出登录 【{{name}}】
          &lt;/a&gt;
      &lt;/span&gt;
&lt;/template&gt;
</code></pre>
<h1 id="打包部署">打包部署</h1>
<p>前端项目需要部署在前端服务器Nginx上,先对tlias项目打包:</p>
<pre><code class="language-json">//package.json
{
"name": "tlias-management",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
    "dev": "vite",
    "build": "run-p type-check \"build-only {@}\" --",//打包
    "preview": "vite preview",
    "build-only": "vite build",
    "type-check": "vue-tsc --build --force"
},
</code></pre>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231226192607.png"></p>
<p>打包后会在项目根路径下生成dist文件夹,这是压缩后的项目。</p>
<p>Nginx是轻量级的Web服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,内存占用少、并发能力强</p>
<h2 id="部署">部署</h2>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231226192935.png"></p>
<ol>
<li>部署:将dist文件夹下的文件复制到nginx安装目录的html目录下</li>
<li>启动:双击nginx.exe</li>
</ol>
<p>直接启动Nginx可能会失败,使用 <code>netstat -ano | findStr 80</code> 查看到System进程占用了80端口</p>
<p><img src="https://obsidian-1320529117.cos.ap-beijing.myqcloud.com/pic/20231226193118.png"></p>
<p>在日志文件中可以看到nginx启动失败,端口被占用。</p>
<p>修改nginx.conf文件夹:</p>
<pre><code class="language-conf">
events {
    worker_connections1024;
}


http {
    include       mime.types;
    default_typeapplication/octet-stream;

    #log_formatmain'$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_loglogs/access.logmain;

    sendfile      on;
    #tcp_nopush   on;

    #keepalive_timeout0;
    keepalive_timeout65;

    #gzipon;

    server {
          listen       88;
          server_namelocalhost;
          client_max_body_size 10m;

          location / {
                root   html;
                indexindex.html index.htm;
          }
          error_page   500 502 503 504/50x.html;
          location = /50x.html {
                  root   html;
          }
        }

}

</code></pre>
<p>将nginx端口修改为88,启动nginx,进行登录会报错404,请求地址:</p>
<pre><code class="language-http">http://localhost:88/api/login
</code></pre>
<p>因为我们的反向代理是在vite上设置的,此处就需要再次设置nginx的反向代理:</p>
<pre><code class="language-conf">server {
    listen       90;
    server_namelocalhost;
    client_max_body_size 10m;


    location / {
      root   html;
      indexindex.html index.htm;
    }

        //反向代理
    location ^~ /api/ {
      rewrite ^/api/(.*)$ /$1 break;
      proxy_pass http://localhost:8080;
    }
       
    error_page   500 502 503 504/50x.html;
    location = /50x.html {
            root   html;
    }
}
</code></pre>
<p>这样就能成功访问了</p><br><br>
来源:https://www.cnblogs.com/euneirophran/p/18073939
頁: [1]
查看完整版本: Tlias-前端开发