简介 Element UI  是一款基于 Vue.js 2.0 的开源桌面端组件库,旨在帮助开发者快速构建现代化、高效且风格统一的 Web 应用程序。ElementPlus 是 Element UI  的官方升级版本,专为 Vue 3.0  设计,完全兼容 Vue 3 的 Composition API 和 TypeScript,并继承了 Element UI 的核心设计理念与组件生态。我们现在使用ElementPlus来实现一套后台管理项目。
项目简介 我们这次实现的项目叫‘realworld’,是一个简单的博客网站;这个项目是一个专门用户练手的demo,有各种语言的实现方式,地址是:https://main--realworld-docs.netlify.app/;我们可以找到数据库表、接口标准,前后端的数据格式等等;我使用Java简单实现了后端项目,代码地址为:https://gitee.com/qiuli-zero/realworld-background-demo.git;前端的代码地址为:https://gitee.com/qiuli-zero/background-management-demo.git
构建项目 新建Vue项目 我们可以参考之前的文章‘Vue3入门’ -‘构建工程项目’,我们这次构建一个名称为‘background-management-demo’的项目
引入ElementPlus 安装ElementPlus依赖 1 npm install element-plus  
全局导入elementplus 修改main.ts
1 2 3 4 import  ElementPlus  from  'element-plus' import  'element-plus/dist/index.css' createApp (App ).use (ElementPlus ).mount ('#app' )
引入ElementPlus图标 安装依赖 1 npm install @element-plus /icons-vue  
全局导入 修改main.ts
1 2 3 4 5 6 7 8 import  * as  ElementPlusIconsVue  from  "@element-plus/icons-vue" ;const  app = createApp (App );for  (const  [key, component] of  Object .entries (ElementPlusIconsVue )) {  app.component (key, component) } app.use (ElementPlus ).mount ('#app' ) 
引入Axios(实现调用后端项目接口) 安装依赖 1 2 npm install axios npm install --save-dev  @types/node 
封装axios工具 在项目的根目录下创建.env文件,注意该文件和package.json同级,在其中定义环境变量
1 2 # 输入你自己的url VITE_APP_BASE_API = 'http://127.0.0.1:4523/m1/7123645-6846544-default/api' 
新建文件‘src/utils/request.ts’,添加如下内容:
1 2 3 4 5 6 7 import  axios from  "axios" ;const  instance = axios.create ({    baseURL : import .meta .env .VITE_APP_BASE_API  as  string , }) export  default  instance;
定义接口所需类型(类似java中的属性类) 我们先来新建用户相关接口,根据官方文档https://main--realworld-docs.netlify.app/specifications/backend/endpoints/中的注册接口,我们来定义入参及返回信息
新建文件‘src/types/index.d.ts’,添加如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export  interface  User  {  username?: string ;   email : string ;   password : string ; } export  interface  UserInfo  {  email : string ;   token?: string ;   username : string ;   bio : string ;   image : string ; } 
封装api 新建文件‘src/api/index.ts’,添加如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import  request from  '@/utils/request' ;import  type  {    User ,     UserInfo  } from  '@/types' ; export  const  register =    (                  params : {             user : User ;         }     ):     	         Promise <{ data : { user : UserInfo  } }> =>         request ({             method : 'POST' ,             url : '/user' ,             data : params,         }); 
在vue中调用api 修改App.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 <template>   <div class="mb-4">     <el-button @click="handleRegister">注册</el-button>   </div> </template> <script setup lang="ts"> import { ref } from 'vue'; import { register } from '@/api'; import type { User } from './types'; const user = ref<User>({   username: 'admin',   password: '123456',   email: "123456@qq.com" }); const handleRegister = async () => {   const res = await register({     user: user.value   });   console.log(res); } </script> 
使用Apifox来mock后端接口 现在我们已经可以调用后端接口了,但是一个关键问题来了,后端接口在哪里?在本项目中,我们搭建了一个后端demo,但是现在有一个更方便的选择,使用Apifox来’仿冒‘一个接口,这个接口有我们定义的url、调用方式(GET、POST等)、参数和返回值,我们在测试时可以先调用它,这样我们可以专注于前端项目;这里简单说说如何使用Apifox
新建项目 
新建接口 新建接口并定义名称、访问方式、URL、参数名称等
新建Mock期望 点击’Mock‘-’新建期望‘
填写期望名称、参数、返回值,点击’保存‘
保存为快捷请求 在上一步保存的期望后面点击’快捷请求‘-’本地Mock‘
在快捷请求页面检查各项参数无误后点击保存
我们点击刚才保存好的快捷请求,现在我们可以看到各项参数,按照参数调用,就可以获取我们设置好的返回值
引入vue-router(实现页面跳转) 安装依赖 创建路由文件 新建文件‘src/router/index.ts’文件,添加如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import  { createRouter, createWebHashHistory } from  "vue-router" ;const  routes = [  {     path : "/" ,     name : "Home" ,     component : () =>  import ("@/views/home/index.vue" ),   },   {     path : "/register" ,     name : "Register" ,     component : () =>  import ("@/views/register/index.vue" ),   } ]; const  router = createRouter ({  history : createWebHashHistory (),   routes, }); export  default  router;
将路由对象引入main.ts中 1 2 3 import  router from  './router' app.use (router).use (ElementPlus ).mount ('#app' ) 
创建页面文件 创建首页 新建文件‘src/views/home/index.vue’,添加如下内容:
1 2 3 4 5 6 7 8 <template>   <div>     this is home page   </div>   <div class="txt-r">     <router-link to="/register">没有账号?去注册</router-link>   </div> </template> 
创建注册页 新建文件‘src/views/register/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 <template>   <div>     <el-form ref="formRef" :rules="rules" :model="user" label-width="86px">       <h3 class="title">系统注册</h3>       <el-form-item label="用户名" prop="username">         <el-input v-model="user.username" placeholder="请输入用户名" prefix-icon="user"></el-input>       </el-form-item>       <el-form-item label="邮箱" prop="email">         <el-input v-model="user.email" placeholder="请输入邮箱" prefix-icon="message"></el-input>       </el-form-item>       <el-form-item label="密码" prop="password">         <el-input v-model="user.password" type="password" placeholder="请输入密码" prefix-icon="lock"></el-input>       </el-form-item>       <el-form-item label>         <el-button type="primary" @click="doRegister">注册</el-button>       </el-form-item>     </el-form>   </div> </template> <script setup lang="ts"> import { useRouter } from 'vue-router'; import { register } from '@/api'; import { ref, computed } from 'vue' import type { User } from '@/types'; const router = useRouter(); const user = ref<User>({   email: '',   username: '',   password: '' }); const doRegister = async () => {   try {     const res = await register({ user: user.value });     console.log(res.data.user);     router.push({ name: 'Home' });   } catch (error) { 	console.error(error)   } } const rules = computed(() => {   return {     username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],     email: [{ required: true, message: '请输入邮箱', trigger: ['change', 'blur'] }],     password: [{ required: true, min: 6, message: '密码最少要6位', trigger: ['change', 'blur'] }],   } })  </script> 
测试 我们启动项目后进入首页
点击链接’没有账号,请注册‘,跳转到注册页面
可以看到已经成功跳转
引入pinia(存储和管理应用状态) Pinia 是 Vue.js 官方推荐的状态管理库 ,它的核心作用是提供一个集中、响应式且易于维护的地方来存储和管理你的应用状态,并支持在组件(页面)之间高效地共享和操作这些数据。我们现在使用pinia来存储和管理我们的登录数据。
安装依赖 引入依赖 修改main.ts文件
1 2 3 4 5 import  { createPinia } from  'pinia' const  pinia = createPinia ()app.use (pinia) 
新增存储文件 新建文件‘src/stores/user.ts’,添加如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import  { ref } from  'vue' import  { defineStore } from  'pinia' import  type  { UserInfo  } from  '@/types' export  const  useUserStore = defineStore ('user' , () =>  {         const  userInfo = ref<UserInfo  | null >(null );          const  setUser  = (user : UserInfo          userInfo.value  = user     };          return  {         userInfo,         setUser     }; }) 
这段代码定义了一个名为 user 的 Pinia Store,用于管理应用程序中的用户信息。 
 
defineStore 是 Pinia 提供的核心函数,用于定义一个 Store。 
第一个参数 ‘user’ 是这个 Store 的唯一 ID。在整个应用程序中,您可以通过这个 ID 来获取和使用这个 Store。 
第二个参数是一个箭头函数 () => { … } ,这个函数定义了 Store 的核心逻辑,包括状态(state)、获取器(getters,这里没有显式定义)和动作(actions)。 
 
const userInfo = ref<UserInfo | null>(null); : 
 
这行代码定义了一个响应式状态 userInfo 。 
ref 是 Vue 3 提供的响应式 API,用于创建一个响应式引用。当 userInfo.value 的值改变时,所有使用 userInfo 的组件都会自动更新。 
<UserInfo | null> 是 TypeScript 的类型注解,表示 userInfo 的值可以是 UserInfo 类型(您之前定义的那个用户接口),也可以是 null 。 
null 是 userInfo 的初始值,表示在应用程序启动时,用户尚未登录或其信息还未被获取。 
 
const setUser = (user: UserInfo) => { userInfo.value = user }; : 
 
这行代码定义了一个名为 setUser 的“动作”(Action)。 
动作是用于修改 Store 状态的方法。 
setUser 接收一个 user 参数,其类型为 UserInfo 。 
它的作用是将传入的 user 对象赋值给 userInfo.value ,从而更新 Store 中的用户状态。 
 
return { userInfo, setUser }; : 
 
这个 return 语句是 defineStore 函数的第二个参数(箭头函数)的返回值。 
它暴露了 userInfo 状态和 setUser 动作,使得其他组件可以通过 userStore 实例来访问和操作它们。 
 
注册时将用户信息保存到store 修改’src/register/index.vue‘文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <script setup lang="ts">     import { useUserStore } from '@/stores/user';     const userStore = useUserStore();     const { setUser } = userStore;     const doRegister = async () => {       try {         console.log('user.value', user.value);         const res = await register({ user: user.value });         console.log(res);         setUser(res.data);         router.push({ name: 'Home' });       } catch (error) {         console.error(error)       }     } </script>     
调用’setUser‘方法,将调用后端接口的返回值’res‘中的数据放入;
注册成功后展示用户信息 再修改’src/home/index.vue‘文件,展示用户信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template>     <div>         <p>this is home page</p>         <p>用户名:{{ userInfo?.username }}</p>         <p>邮箱:{{ userInfo?.email }}</p>     </div>     <div class="txt-r" v-if="!userInfo">         <router-link to="/register">没有账号,请注册</router-link>     </div> </template> <script setup> import { storeToRefs } from "pinia" import { useUserStore } from '@/stores/user'; const userStore = useUserStore(); const { userInfo } = storeToRefs(userStore); </script> 
测试 我们刷新页面来到主页
点击链接跳转到注册页面
填写资料
点击注册跳转回首页
可以看到用户信息已经被渲染出来了
使用localStorage存储用户信息 	现在我们实现了用户登录,但是只要刷新一下页面,用户信息就没有了,这是怎么回事呢?这是 Vue 单页面应用(SPA)开发中一个非常典型的情况:页面刷新后,Vue 应用的内存状态(例如 Vuex 或组件 data 中的数据)会被重置,导致登录状态和用户信息丢失 。
简单来说,虽然登录后拿到了用户信息,但如果只把它们保存在 Vue 组件或 Vuex 的状态管理里,这些数据就像暂时记在电脑内存里,浏览器一刷新,内存清空,数据自然就没了。
解决的方案就是数据的持久化,解决方法的核心思路是:将关键数据保存在一个刷新后也不会丢失的地方(持久化),并在页面初始化时将其读回内存 。我们选择使用**localStorage**存储登录令牌’token‘;
封装storage存取 新建文件‘src/utils/storage.ts’,添加如下内容,在文件中封装localStorage的操作
1 2 3 4 5 6 7 8 9 10 11 12 export  const  storage  = (key : string     get<T>(): T | null  {         const  item = window .localStorage .getItem (key);         return  item ? JSON .parse (item) : null ;     },     set<T>(value : T) {         window .localStorage .setItem (key, JSON .stringify (value));     },     remove (         window .localStorage .removeItem (key);     }, }); 
新增接口调用方法getUser 在文件’src/api/index.ts‘中添加接口调用方法
1 2 3 4 5 6 7 export  const  getCurrentUser =    ():         Promise <{ data : { user : UserInfo  } }> =>         request ({             method : 'GET' ,             url : `/user` ,         }); 
新增身份验证方法 在文件’src/stores/user.ts‘中添加身份验证方法
1 2 3 4 5 6 const  verifyAuth  = async  (        if  (!userInfo.value  && userStorage.get ()) {             const  res = await  getUser ();             setUser (res.data .user )         }     } 
页面刷新时重新加载用户信息 修改文件’main.ts‘
1 2 3 4 5 import  useUserStore from  '@/stores/user' ;const  userStore = useUserStore ();const  { verifyAuth } = userStore;await  verifyAuth ();
axios请求拦截器在请求头中追加token 修改文件’src/utils/request.tx‘
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import  { storage } from  "@/utils/storage" ;instance.interceptors .request .use (     (config ) =>  {         const  userStorage = storage ('user' );         const  token = userStorage.get ();         if  (token) {             const  newConfig = { ...config };             newConfig.headers .Authorization  = `Bearer ${token} ` ;             return  newConfig;         }         return  config;     },     (error ) =>  Promise .reject (error) ) 
可以看到,我们在请求头(headers)中添加了一个名称为‘Authorization’的token,而且该token是以‘Bearer’开头的
页面整体布局 我们的页面整体布局如下图:
从左侧的栏目中我们可以看到有’文章管理‘、’评论管理‘、’个人设置‘3个类别,类别’文章管理‘中有’全部文章‘、’我的文章‘两个页面,我们就先从这里入手编写页面子路由;
页面子路由 首先我们来实现页面间的跳转,我们现在新定义三个页面:’整体布局‘、’全部文章‘和’我的文章‘,当访问根路径(http://localhost:5173/)时直接重定向到’整体布局‘页面(默认加载’全部文章‘子页面),同时’整体布局‘页面中有指向’全部文章‘、’我的文章‘页面的链接,点击链接时加载对应的子页面 
新增’全部文章‘、’我的文章‘两个页面 新建文件‘src/views/articles/AllArticles.vue’和‘src/views/articles/MyArticles.vue’两个文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <!--AllArticles.vue--> <template>     <p>AllArticles</p> </template> <script setup></script> <style scoped></style> <!-- MyArticles.vue --> <template>     <p>MyArticles</p> </template> <script setup></script> <style scoped></style> 
新增layout页面 新建文件‘src/views/layout/index.vue’,添加如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template>     <router-link to="/article/all">         全部文章     </router-link>     <router-link to="/article/me">         我的文章     </router-link>     <div>         <router-view></router-view>     </div> </template> <script setup></script> <style scoped></style> 
修改路由规则 接下来修改路由,我们希望当进入本项目时(即访问根目录时)直接进入’文章管理‘-’全部文章‘,点击’我的文章‘时切换到’我的文章‘页面
修改’src/router/index.ts‘文件
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 import  { createRouter, createWebHashHistory } from  "vue-router" ;import  Layout  from  "@/views/layout/index.vue" ;const  routers = [    {         path : "/" ,         name : "Home" ,         component : Layout ,          redirect : '/article' ,          children : [              {                 path : "article" ,                  name : "文章管理" ,                 meta : {                      requiresAuth : true ,                      icon : "user"                   },                 redirect : '/article/all' ,                  children : [                      {                         path : "/article/all" ,                          name : "全部文章" ,                         meta : {                             requiresAuth : true ,                             icon : "avatar"                          },                         component : () =>  import ("@/views/articles/AllArticles.vue" ),                      },                     {                         path : "/article/me" ,                         name : "我的文章" ,                         meta : {                             requiresAuth : true ,                             icon : "avatar"                          },                         component : () =>  import ("@/views/articles/MyArticles.vue" ),                     }                 ],             },         ]     } ] const  router = createRouter ({    history : createWebHashHistory (),     routes : routers, }) export  default  router;
我们稍微解释一下整个逻辑
访问根路径 /时,会重定向 到 /article(由 redirect属性指定)。 
根路径使用 Layout组件作为布局。 
children数组定义了嵌套路由 。这意味着当路径匹配时,<router-view>会被渲染到 Layout组件中预留的 <router-view>位置,从而实现页面布局和内容的嵌套。在 文章管理路由下,又定义了一层 children,形成了嵌套路由的嵌套 。全部文章和 我的文章是 文章管理的子页面。 
meta字段包含了路由元信息 ,这里标记了这些路由需要认证 (requiresAuth: true),可用于路由守卫进行权限检查。icon可能用于在导航菜单中显示图标。 
测试 我们重新启动项目,当我们访问’http://localhost:5173/‘时,重定向到’http://localhost:5173/#/article/all‘ 
当我们点击链接’我的文章‘,会重定向到’我的文章‘页面
我们来实现侧方导航栏,导航栏中实现上一步实现的子页面跳转,需要用到的组件有’ele-menu‘、’el-sum-menu‘、’el-menu-item‘
新增导航栏组件 新建文件‘src/layout/components/PageSidebar.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 <template>     <div>         <!-- router : 属性启用路由模式 -->         <!-- :collapse="isCollapse" : 控制侧边栏折叠状态 -->         <el-menu :default-active="defaultActive" router :collapse="isCollapse">             <template v-for="(item, i) in treeData" :key="item.path">                 <el-sub-menu :index="item.path" v-if="item.children && item.children.length > 0">                     <template #title>                         <el-icon v-if="item.meta.icon">                             <component :is="item.meta.icon"></component>                         </el-icon>                         <span>{{ item.name }}</span>                     </template>                     <template v-for="(child, ci) in item.children" :key="ci">                         <el-menu-item :index="child.path">                             <el-icon>                                 <component :is="child.meta.icon"></component>                             </el-icon>                             {{ child.name }}                         </el-menu-item>                     </template>                 </el-sub-menu>             </template>         </el-menu>     </div> </template> <script setup> import { computed, ref } from 'vue'; import { useRouter, useRoute } from 'vue-router'; // 全局路由器实例 获取 Vue Router 的全局路由器实例 const router = useRouter(); // 当前路由对象 // 包含信息: // - path - 当前路由路径 // - params - 动态路由参数 // - query - URL 查询参数 // - hash - URL 哈希值 // - name - 路由名称(如果配置了) // - meta - 路由元信息 const route = useRoute(); // 菜单数据生成 // 1. 从 Vue Router 获取所有路由配置 // 2. 过滤出需要认证的路由( meta.requiresAuth 为 true) // 3. 自动构建菜单树结构 const treeData = router.getRoutes().filter((v) => v.meta && v.meta.requiresAuth); // 活动菜单项控制 // 使用 computed 属性动态计算当前活动菜单 // 优先使用当前路由路径,否则使用第一个菜单项的路径 const defaultActive = computed(() => route.path || treeData.value[0].path) // 响应式功能 // 使用 ref 管理侧边栏折叠状态 const isCollapse = ref(false) </script> <style scoped></style> 
这个组件的整体逻辑是利用全局路由对象’useRouter()‘和当前路由对象’useRoute()‘生成导航栏,将路由中的信息遍历后渲染(赋值)给导航栏组件
修改布局组件 修改’src/views/layout/index.vue‘
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template>     <div>         <page-sidebar></page-sidebar>     </div>     <div>         <router-view></router-view>     </div> </template> <script setup> import PageSidebar from '@/views/layout/components/PageSidebar.vue' </script> <style scoped></style> 
这里可能你会有个疑问,明明导入的是’PageSidebar‘,怎么在template中变成了’page-sidebar’标签;这里我们介绍一下Vue的自动转换机制:1. 导入时使用 PascalCase :这是 JavaScript/TypeScript 的命名约定;2. 模板中使用 kebab-case :这是 HTML 的命名约定,更符合 HTML 标准;3. Vue 自动处理转换 :Vue 编译器会自动将 PascalCase 组件名转换为 kebab-case 标签名;
简单来说,Vue会自动帮助我们将导入的组件转换为从大小写处用’-‘分开的标签
测试 现在让我们来看看效果
可以看到,我们需要的效果达成了
美化样式 删除项目中之前引入的样式 修改文件style.css(与main.ts同级)
修改App.vue中的样式 修改App.vue文件(与main.ts同级)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <style> html, body {   height: 100%;   margin: 0;   padding: 0; } #app {   height: 100%;   width: 100%;   /* 隐藏超出容器范围的内容,防止滚动条出现 */   overflow: hidden; } </style> 
注意删除’scoped‘,否则配置的style只能在该组件中生效 
 
安装sass 简单介绍一下sass:Sass(Syntactically Awesome Stylesheets)是一款CSS预处理器 (CSS预编译器)
。它扩展了CSS的基础功能,为你提供了变量、嵌套规则、混合宏(mixins)、函数等强大的编程特性,旨在让编写和维护样式代码更高效、更优雅
修改侧边导航栏页面 修改’src/views/layout/components/PageSidebar.vue’
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template>     <div>         <div style="text-align: center;">             <span class="cursor" @click="isCollapse = !isCollapse">                 <el-icon v-if="isCollapse">                     <expand></expand>                 </el-icon>                 <el-icon v-else>                     <fold></fold>                 </el-icon>             </span>         </div>         <el-menu background-color="#000" text-color="#fff" :default-active="defaultActive" router:collapse="isCollapse">             。。。代码不变         </el-menu>     </div> </template> ...... 照旧不变 
我们为导航栏添加了一个折叠功能
修改布局页面 修改’src/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 <template>     <div class="page-container">         <header>我是Header</header>         <main>             <div class="left">                 <page-sidebar></page-sidebar>             </div>             <div class="right">                 <router-view></router-view>             </div>         </main>     </div> </template> <script setup> import PageSidebar from '@/views/layout/components/PageSidebar.vue' </script> <style lang="scss"> .page-container {     display: flex;     flex-direction: column;     height: 100vh; /* 使用视口高度确保填满整个屏幕 */     width: 100%;     overflow: hidden;     >header {         height: 54px;         background: #000;         color: #fff;         flex-shrink: 0;     }     >main {         display: flex;         flex: 1;         overflow: hidden;         >.left {             height: 100%;             background-color: #000;             color: #fff;         }         >.right {             flex: 1;             overflow: auto;             background-color: #f5f7f9;             >.main-body {                 padding: 16px 16px 30px;                 overflow: auto;                 height: 100%;                 box-sizing: border-box;             }         }     } } </style> 
修改注册页面 修改’src/views/register/index.vue‘
1 2 3 4 5 6 7 8 9 10 11 12 13 <template>   <div>     <el-form class="reg" ref="formRef" :rules="rules" :model="user" label-width="86px">       。。。 </template> <style lang="scss" scoped> .reg {   width: 480px;   margin: 200px auto 0;   text-align: center; } </style> 
测试 
我们现在想在’Header‘的右角添加显示用户信息和注销的按钮
修改’src/stores/user.ts‘文件 1 2 3 4 5 6 7 const  isLoggedIn = computed (() =>  !!userInfo.value );const  removeUser  = (	userInfo.value  = null ; 	userStorage.remove (); }; 
新增页眉页面 新增文件’src/views/layout/components/PageHeader.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 <template>     <div class="header-cont">         <div>             <h1>                 <router-link to="/">                     <!-- {{ userInfo.value?.name || '后台管理系统' }} -->                     后台管理系统                 </router-link>             </h1>         </div>         <div>             <template v-if="isLoggedIn">                 <el-dropdown trigger="click" @command="handleCommand">                     <div>                         {{ userInfo.username }}                         <el-icon>                             <caret-bottom />                         </el-icon>                     </div>                     <template #dropdown>                         <el-dropdown-menu>                             <el-dropdown-item command="toPersonal">个人信息</el-dropdown-item>                             <el-dropdown-item command="toLogout">Logout</el-dropdown-item>                         </el-dropdown-menu>                     </template>                 </el-dropdown>             </template>             <template v-if="!isLoggedIn">                 <el-button type="primary" @click="router.push('/register')">注册/登录</el-button>             </template>         </div>     </div> </template> <script setup> import { ref } from 'vue'; import { computed } from 'vue'; import { useRouter } from 'vue-router'; const router = useRouter(); import useUserStore from '@/stores/user'; const userStore = useUserStore(); const { removeUser } = userStore; import { storeToRefs } from 'pinia'; const { userInfo, isLoggedIn } = storeToRefs(userStore); const commands = ({     toPersonal: () => {         console.log('toPersonal')     },     toLogout: () => {         console.log('toLogout')         removeUser();         router.push('/register');     } }) function handleCommand(command) {     commands[command] && commands[command](); } </script> <style scoped> .header-cont {     display: flex;     align-items: center;     justify-content: space-between;     padding: 0 20px;     height: 100%;     a {         color: inherit;         text-decoration: none;     }     h1 {         margin: 0;         font-size: 20px;     } } </style> 
我们在页眉组件中定义了一个下拉菜单,当判断用户已经注册(登录),就显示这个下拉菜单,如果判断用户没有注册,就显示一个’注册/登录‘按钮;在下拉菜单中,我们设置两个属性,分别是’个人信息‘和’注销(Logout)‘;设置函数’handleCommand‘,当点击不同属性,发送不同命令触发不同的处理方法
修改布局页面 修改’src/views/layout/index.vue’
我们在布局组件中添加新增的PageHeader.vue
1 2 3 4 5 6 7 8 9 10 11 <template>     <div class="page-container">         <header>             <page-header></page-header>         </header>     </div> </template> <script setup> import PageHeader from '@/views/layout/components/PageHeader.vue'; </script> 
测试 首先我们重启项目,使数据清空,然后访问根目录,根据我们写的路由,会重定向到’布局‘页面
接着我们点击’注册/登录‘按钮,进入注册页面,填写信息
点击’注册‘按钮,跳转到’布局‘页面,此时已经完成注册,页眉处显示下拉菜单
登录/个人信息模块 个人信息模块 现在我们页眉(Header)组件中下拉菜单的’个人信息‘选项是没有作用的,这是因为我们还没有编辑个人信息页面,现在我们希望实现:1. 创建个人信息页面,在其中可以查看和修改个人信息; 2. 在侧方导航栏中加入’个人设置‘-’个人信息‘路由;3. 点击下拉菜单的’个人信息‘选项,跳转到’个人信息‘页面;
我们先看看个人信息页面应该是什么样子:
新增修改个人信息接口 我们首先新增一个’修改个人信息‘的接口,修改文件’src/api/index.ts‘, 添加接口
1 2 3 4 5 6 7 8 9 10 11 12 13 export  const  updateUser =    (         params : {             user : UserInfo ;         }     ):         Promise <{ data : { user : UserInfo  } }> =>         request ({             method : 'POST' ,             url : '/updateUser' ,             data : params,         }); 
新增’个人信息‘页面 创建文件’src/views/personal/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 <template>     <div class="main-body">         <el-form ref="formRef" class="profile-form" v-model="user" label-width="86px">             <h3>个人信息</h3>             <el-form-item label="用户名">                 <el-input v-model="user.username" placeholder="请输入用户名" prefix-icon="user"></el-input>             </el-form-item>             <el-form-item label="头像">                 <el-input v-model="user.image" placeholder="请输入头像" prefix-icon="picture"></el-input>             </el-form-item>             <el-form-item label="简介">                 <el-input v-model="user.bio" placeholder="请输入简介" prefix-icon="user"></el-input>             </el-form-item>             <el-form-item label="邮箱">                 <el-input v-model="user.email" placeholder="请输入邮箱" prefix-icon="user"></el-input>             </el-form-item>             <el-form-item label="密码">                 <el-input v-model="user.password" type="password" placeholder="请输入密码" prefix-icon="lock"></el-input>             </el-form-item>             <el-form-item label>                 <el-button type="primary" @click="handlerUpdateUser" class="w100p">                     修改                 </el-button>             </el-form-item>         </el-form>     </div> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue'; import { storeToRefs } from 'pinia'; import useUserStore from '@/stores/user'; import { updateUser } from '@/api'; import type { UserInfo } from '@/types'; import { ElMessageBox } from 'element-plus'; const userStore = useUserStore(); const { userInfo } = storeToRefs(userStore); const { setUser } = userStore; const user = ref<UserInfo>({     email: '',     password: '',     username: '',     bio: '',     image: '', }); const handlerUpdateUser = async () => {     try {         console.log('user.value', user.value);         const res = await updateUser({ user: user.value });         setUser(res.data.user);         console.log('update user success', res.data.user);         ElMessageBox.alert('更新成功', '修改用户', {             confirmButtonText: 'OK'         });     } catch (error) {         console.error(error)     } } onMounted(() => {     if (userInfo.value) {         user.value = {             email: userInfo.value.email,             password: '',             username: userInfo.value.username,             bio: userInfo.value.bio,             image: userInfo.value.image         }     } }) </script> <style scoped lang="scss"> .profile-form {     width: 60%;     margin-right: auto; } </style> 
添加子路由 修改文件’src/router/index.ts‘,在路由中添加如下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 {         path: "/personal",         name: "个人设置",         meta: {             // 路由元信息             requiresAuth: true, // 需要认证             icon: "setting" // 图标         },         children: [             {                 path: "/personal/me",                 name: "个人信息",                 meta: {                     requiresAuth: true,                     icon: "chat-dot-round"                 },                 component: () => import("@/views/personal/index.vue")             },         ],     } 
修改文件’src/views/layout/components/PageHeader.vue‘,添加子路由跳转
1 2 3 4 toPersonal: () => {         console.log('toPersonal')         router.push('/personal/me');     }, 
测试 访问根路径(http://localhost/5173),看到侧方导航栏已经有’个人设置‘-’个人信息‘路由,点击可以跳转到’个人信息‘页面(点击页眉的’个人信息‘选项也可以跳转) 
登录模块 我们现在只写了注册页面,现在我们来写一个必不可少的登录页面,我们来看看登录页面什么样子
新增登录接口 修改文件’src/api/index.ts‘,添加登录接口调用
1 2 3 4 5 6 7 8 9 10 11 12 13 export  const  doLogin =    (         params : {             user : User ;         }     ):         Promise <{ data : { user : UserInfo  } }> =>         request ({             method : 'POST' ,             url : '/user/login' ,             data : params,         }); 
新增登录页面 新建文件’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 <template>     <div>         <el-form class="login" ref="formRef" :model="user" :rules="rules" label-width="86px">             <h3>登录</h3>             <el-form-item label="用户名" prop="email">                 <el-input v-model="user.email" placeholder="请输入用户名" prefix-icon="user"></el-input>             </el-form-item>             <el-form-item label="密码" prop="password">                 <el-input v-model="user.password" type="password" placeholder="请输入密码" prefix-icon="lock"></el-input>             </el-form-item>             <el-form-item label>                 <el-button type="primary" class="w100p" @click="doLogin">登录</el-button>             </el-form-item>             <div class="txt-r">                 <router-link to="/register">没有账号?去注册</router-link>             </div>         </el-form>     </div> </template> <script setup lang="ts"> import { useRouter } from 'vue-router'; const router = useRouter(); import useUserStore from '@/stores/user'; const userStore = useUserStore(); const { setUser } = userStore; import { computed, reactive, ref } from 'vue'; import type { User } from '@/types'; const user = reactive<User>({     email: '',     password: '' }) const formRef = ref(); const rules = computed(() => {     return {         email: [{ required: true, message: '请输入用户名', trigger: ['change', 'blur'] }],         password: [{ required: true, min: 6, message: '请输入密码', trigger: ['change', 'blur'] }],     } }) import { login } from '@/api'; function doLogin() {     console.log(formRef.value);     formRef.value.validate(async (valid: any) => {         if (!valid) {             return;         }         try {             console.log('user', user);             const res = await login({ user: user });             console.log(res);             setUser(res.data.user);             router.push({ name: 'Home' });         } catch (error) {             console.error(error);         }     }) } </script> <style lang="scss" scoped> .login {     width: 480px;     margin: 200px auto 0;     text-align: center; } </style> 
添加子路由 修改文件’src/router/index.ts‘
1 2 3 4 5 {         path : "/login" ,         name : "登录" ,         component : () =>  import ("@/views/login/index.vue" )     } 
添加导航守卫 修改文件’src/router/index.ts‘,添加如下代码:
1 2 3 4 5 6 7 8 import  useUserStore from  '@/stores/user' ;router.beforeEach (async  (to) => {     const  userStore = useUserStore ();     const  { isLoggedIn } = userStore;     if  (to.meta .requiresAuth  && !isLoggedIn) {         return  { name : '登录'  };     } }) 
这个函数是一个 Vue Router 全局前置守卫 ,用于在路由跳转前进行权限验证。执行流程如下:
获取用户状态 :从 Pinia store 中获取用户登录状态 
检查路由权限 :判断目标路由是否需要认证 ( to.meta.requiresAuth ) 
验证登录状态 :如果路由需要认证但用户未登录 ( !isLoggedIn ) 
重定向到登录页 :返回登录页面的路由配置 
 
测试 我们注册后,直接访问登录页面’http://localhost:5173/#/login‘ 
输入用户名(邮箱)和密码,然后点击登录,登录成功后跳转到根目录(我们已经将根目录重定向到’全部文章‘页面),如果没有登录(可以点击页眉右侧的’注销‘按钮),无论访问哪个页面,都会重定向到’登录‘页面
文章模块 显示文章(分页查询) 现在我们处理好了注册、登录页面,但是文章页面还没有内容显示,我们希望能将文章内容显示出来,如下所示:
新增文章查询接口 修改文件’src/types/index.d.ts‘,新增文章数据类型
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 export  interface  CreateArticle  {    title : string ;     description : string ;     body : string ;     tagList : string []; } export  interface  Article  extends  CreateArticle  {    slug : string ;     createdAt : string ;     updatedAt : string ;     favorited : boolean ;     favoritesCount : number ;     author : Author ; } export  interface  ArticleSearchParams  {         tag?: string ;     author?: string ;     favorited?: string ;     offset?: number ;     limit?: number ; } export  interface  PageInfo  {    currentPage : number ;     pageSize : number ;     total : number ;     articles : Article []; } 
修改文件’src/api/index.ts‘,新增’查询文章信息‘接口,我们这里的文章查询接口是一个分页查询
1 2 3 4 5 6 7 8 9 10 11 export  const  getArticles =    (params : ArticleSearchParams ):         Promise <{ data : { articles : Article []; articlesCount : number ; }; }> => {         return  request ({             method : 'GET' ,             url : '/articles' ,                          params,         })     } 
修改’全部文章‘页面 修改文件’src/views/articles/AllArticles.vue‘,调用’文章查询‘接口查询文章信息,并将其填充到<el-table>中,并使用el-pagination作为分页组件
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 <script setup lang="ts"> import { ref } from 'vue'; import { getArticles } from '@/api'; import type { ArticleSearchParams, PageInfo } from '@/types'; let pageArticles = ref<PageInfo>({     currentPage: 1,     pageSize: 10,     total: 0,     articles: [] }); const fetchArticles = async (offset: number, pageSize: number) => {     const params: ArticleSearchParams = {         'offset': offset,         'limit': pageSize     };     try {         console.log(params);         const res = await getArticles(params);         console.log(res);         pageArticles.value.articles = res.data.articles;         pageArticles.value.total = res.data.articlesCount;     } catch (error) {         console.error(error);     } }; const handleSizeChange = (val: number) => {     pageArticles.value.pageSize = val;     fetchArticles((pageArticles.value.currentPage - 1) * val, val); }; const handleCurrentChange = (val: number) => {     pageArticles.value.currentPage = val;     fetchArticles((val - 1) * pageArticles.value.pageSize, pageArticles.value.pageSize); }; fetchArticles(0, pageArticles.value.pageSize); </script> <template>     <div class="main-body">         <el-table :data="pageArticles.articles" style="width: 100%;" border>             <el-table-column prop="slug" label="ID" width="240" :showOverflowTooltip="true"></el-table-column>             <el-table-column prop="title" label="标题" width="240" :showOverflowTooltip="true"></el-table-column>             <el-table-column prop="createdAt" label="创建时间" width="240" :showOverflowTooltip="true"></el-table-column>             <el-table-column prop="author" label="作者"></el-table-column>             <el-table-column label="标签" align="center" prop="tagList">                 <template #default="scope">                     <el-tag class="a_tag" v-for="tag in scope.row.tagList">                         {{ tag }}                     </el-tag>                 </template>             </el-table-column>         </el-table>         <!--分页组件-->         <el-pagination v-model:current-page="pageArticles.currentPage" :page-size="pageArticles.pageSize"             :page-sizes="[5, 10, 15, 20, 25, 30]" :total="pageArticles.total" layout="total, sizes, prev, pager, next"             @size-change="handleSizeChange" @current-change="handleCurrentChange" />     </div> </template> <style lang="scss" scoped> .pagination {     margin-top: 20px;     float: right } .a_tag {     margin: 0 3px; } </style> 
测试 
添加分页查询条件 文章多了,我们当然需要按照一定条件进行查询过滤,这里我们选择用作者和标签进行过滤
添加标签查询接口 在文件‘src/api/index.ts’中添加标签查询接口
1 2 3 4 5 6 7 8 9 export  const  getTags =    ():         Promise <{ data : { tags : string [] }; }> => {         return  request ({             method : 'GET' ,             url : '/tags' ,         })     } 
修改‘全部文章’页面 修改文件‘src\views\articles\AllArticles.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 <script setup lang="ts"> import { ref, onMounted } from 'vue'; import { getArticles, getTags } from '@/api'; import type { ArticleSearchParams, PageInfo } from '@/types'; let pageArticles = ref<PageInfo>({     currentPage: 1,     pageSize: 10,     total: 0,     articles: [] }); const filters = ref({     name: '' }) const selectedValue = ref<string | undefined>(); const tags = ref<string[]>([]); const doSearch = async () => {     pageArticles.value.pageSize = 10;     pageArticles.value.currentPage = 1;     fetchArticles(pageArticles.value.pageSize * (pageArticles.value.currentPage - 1), pageArticles.value.pageSize); } const fetchArticles = async (offset: number, pageSize: number) => {     const params: ArticleSearchParams = {         'offset': offset,         'limit': pageSize     };     if (filters.value.name !== '') {         params['author'] = filters.value.name;     }     if (selectedValue.value !== undefined) {         params['tag'] = selectedValue.value;     }     try {         console.log(params);         const res = await getArticles(params);         console.log(res);         pageArticles.value.articles = res.data.articles;         pageArticles.value.total = res.data.articlesCount;     } catch (error) {         console.error(error);     } }; const handleSizeChange = (val: number) => {     pageArticles.value.pageSize = val;     fetchArticles((pageArticles.value.currentPage - 1) * val, val); }; const handleCurrentChange = (val: number) => {     pageArticles.value.currentPage = val;     fetchArticles((val - 1) * pageArticles.value.pageSize, pageArticles.value.pageSize); }; onMounted(async () => {     try {         const res = await getTags();         console.log(res);         tags.value = res.data.tags;     } catch (error) {         console.error(error);     } }); fetchArticles(0, pageArticles.value.pageSize); </script> <template>     <div class="main-body">         <!--工具栏-->         <div class="toolbar">             <el-form :inline="true" :model="filters">                 <el-form-item>                     <el-input v-model="filters.name" placeholder="请输入作者"></el-input>                 </el-form-item>                 <el-form-item>                     <el-select v-model="selectedValue" placeholder="请选择标签" style="width: 240px;">                         <el-option v-for="(option, index) in tags" :key="index" :label="option"                             :value="option"></el-option>                     </el-select>                 </el-form-item>                 <el-form-item>                     <el-button icon="search" type="primary" @click="doSearch">查询</el-button>                 </el-form-item>             </el-form>         </div>         <!--文章列表---->         <el-table :data="pageArticles.articles" style="width: 100%;" border>             <el-table-column prop="slug" label="ID" width="240" :showOverflowTooltip="true"></el-table-column>             <el-table-column prop="title" label="标题" width="240" :showOverflowTooltip="true"></el-table-column>             <el-table-column prop="createdAt" label="创建时间" width="240" :showOverflowTooltip="true"></el-table-column>             <el-table-column prop="author.username" label="作者"></el-table-column>             <el-table-column label="标签" align="center" prop="tagList">                 <template #default="scope">                     <el-tag class="a_tag" v-for="tag in scope.row.tagList">                         {{ tag }}                     </el-tag>                 </template>             </el-table-column>         </el-table>         <!--分页组件-->         <el-pagination v-model:current-page="pageArticles.currentPage" :page-size="pageArticles.pageSize"             :page-sizes="[5, 10, 15, 20, 25, 30]" :total="pageArticles.total" layout="total, sizes, prev, pager, next"             @size-change="handleSizeChange" @current-change="handleCurrentChange" />     </div> </template> <style lang="scss" scoped> .pagination {     margin-top: 20px;     float: right } .a_tag {     margin: 0 3px; } </style> 
测试 进入文章页面,输入作者名称,点击‘查询’,可以看到查找出的对应文章
点击下拉菜单‘请选择标签’,可以看到所有标签名称
选择对应的标签,点击‘查询’,可以看到查询出的对应文章
文章的增删改查 我们现在来实现文章的增删该查,在‘我的文章’页面,我们需要实现的功能如下:(1)点击侧边栏‘我的文章’,展示出当前登录用户添加的文章(分页展示);(2)点击添加文章按钮,弹出添加文章页面,编辑文章信息后,点击确定,实现添加文章功能;(3)点击编辑按钮,弹出编辑文章页面(就是添加文章页面,但是要带出当前文章信息),编辑完成后,点击确定按钮保存;(4)点击删除按钮,删除对应文章
新增接口 修改文件‘src/api/index.ts’,添加接口
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 export  const  getArticleBySlug = (slug : string ): Promise <{ data : { article : Article  } }> => {    return  request ({         method : 'GET' ,         url : `/articles/${slug} ` ,     }) }; export  const  deleteArticleBySlug = (slug : string ): Promise <void > => {    return  request ({         method : 'DELETE' ,         url : `/articles/${slug} ` ,     }) }; export  const  updateArticle = (    slug : string ,     params : {         article : UpdateArticle ;     } ): Promise <{ data : { article : Article  } }> => {     return  request ({         method : 'PUT' ,         url : `/articles/${slug} ` ,         data : params,     }) }; export  const  createArticle = (    params : { article : CreateArticle  } ): Promise <{ data : { article : Article  } }> =>     request ({         method : 'POST' ,         url : '/articles' ,         data : params,     }); 
修改‘我的文章’页面 修改文件‘src/views/articles/MyArticles.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 <script setup lang="ts"> import type { CreateArticle, PageInfo } from '@/types'; import { createArticle, getArticles, getArticleBySlug, deleteArticleBySlug, updateArticle } from '@/api'; import { onBeforeMount, ref } from 'vue'; import { storeToRefs } from 'pinia'; import useUserStore from '@/stores/user'; import type { ArticleSearchParams } from '@/types'; import { el, tr } from 'element-plus/es/locales.mjs'; import { ElMessageBox } from 'element-plus'; const createForm = ref<CreateArticle>({     title: '',     description: '',     body: '',     tagList: [], }); let pageArticles = ref<PageInfo>({     currentPage: 1,     pageSize: 10,     total: 0,     articles: [] }); const article_slug = ref(''); const createVisible = ref(false); const title = ref('新增文章'); const tag = ref(''); const addArticleTag = () => {     if (tag.value === '') {         return;     }     createForm.value.tagList.push(tag.value);     tag.value = ''; }; const delArticleTag = (index: number) => {     createForm.value.tagList.splice(index, 1); } const handleCreateArticle = async () => {     try {         title.value = '新增文章';         createForm.value.title = '';         createForm.value.description = '';         createForm.value.body = '';         createForm.value.tagList = [];         article_slug.value = '';         createVisible.value = true;     } catch (error) {         console.error(error);     } }; const handleClose = () => {     createVisible.value = false; }; const handleConfirm = async () => {     if (article_slug.value === '') {         const res = await createArticle({ article: createForm.value });         console.log(res);     } else {         const res = await updateArticle(article_slug.value, { article: createForm.value });         console.log(res);     }     // 关闭窗口     handleClose();     // 渲染文章列表     fetchArticles(0, pageArticles.value.pageSize); }; const handleSizeChange = (val: number) => {     pageArticles.value.pageSize = val;     fetchArticles((pageArticles.value.currentPage - 1) * val, val); }; // 执行编辑文章 const handleEdit = async (slug: string) => {     try {         title.value = '编辑文章';         createVisible.value = true;         const res = await getArticleBySlug(slug as string);         createForm.value = res.data.article;         article_slug.value = slug;     } catch (error) {         console.error(error);     } } // 执行删除文章 const handleDelete = async (slug: string) => {     ElMessageBox.confirm('将要删除本条记录,是否继续?', '删除文章', {         confirmButtonText: '确定',         cancelButtonText: '取消',         type: 'warning',         draggable: true,     }).then(async () => {         try {             await deleteArticleBySlug(slug);             fetchArticles(0, pageArticles.value.pageSize);         } catch (error) {             console.error(error);         }     }) } const handleCurrentChange = (val: number) => {     pageArticles.value.currentPage = val;     fetchArticles((val - 1) * pageArticles.value.pageSize, pageArticles.value.pageSize); }; const fetchArticles = async (offset: number, pageSize: number) => {     const userStore = useUserStore();     const { userInfo } = storeToRefs(userStore);     const params: ArticleSearchParams = {         'offset': offset,         'limit': pageSize,         'author': userInfo.value?.username || '',     };     try {         const res = await getArticles(params);         pageArticles.value.articles = res.data.articles;         pageArticles.value.total = res.data.articlesCount;     } catch (error) {         console.error(error);     } }; fetchArticles(0, pageArticles.value.pageSize); </script> <template>     <div class="main-body">         <!--工具栏-->         <el-form :inline="true">             <el-form-item>                 <el-button type="primary" @click="handleCreateArticle">添加文章</el-button>             </el-form-item>         </el-form>         <!--文章列表---->         <el-table :data="pageArticles.articles" style="width: 100%;" border>             <el-table-column prop="slug" label="ID" width="240" :showOverflowTooltip="true"></el-table-column>             <el-table-column prop="title" label="标题" width="240" :showOverflowTooltip="true"></el-table-column>             <el-table-column prop="createdAt" label="创建时间" width="240" :showOverflowTooltip="true"></el-table-column>             <el-table-column prop="author.username" label="作者"></el-table-column>             <el-table-column label="标签" align="center" prop="tagList">                 <template #default="scope">                     <el-tag class="a_tag" v-for="tag in scope.row.tagList">                         {{ tag }}                     </el-tag>                 </template>             </el-table-column>             <el-table-column label="操作">                 <template #default="scope">                     <el-button link type="primary" @click="handleEdit(scope.row.slug)">编辑</el-button>                     <el-button link type="danger" @click="handleDelete(scope.row.slug)">删除</el-button>                 </template>             </el-table-column>         </el-table>         <!--分页组件-->         <el-pagination v-model:current-page="pageArticles.currentPage" :page-size="pageArticles.pageSize"             :page-sizes="[5, 10, 15, 20, 25, 30]" :total="pageArticles.total" layout="total, sizes, prev, pager, next"             @size-change="handleSizeChange" @current-change="handleCurrentChange" />     </div>     <!--新增文章页面-->     <el-dialog v-model="createVisible" :title="title" width="500px" :before-close="handleClose">         <el-form ref="formRef" :model="createForm" label-width="80px">             <el-form-item label="文章标题">                 <el-input v-model="createForm.title" placeholder="请输入文章标题"></el-input>             </el-form-item>             <el-form-item label="文章简介">                 <el-input v-model="createForm.description" placeholder="请输入文章简介"></el-input>             </el-form-item>             <el-form-item label="文章内容">                 <el-input v-model="createForm.body" placeholder="请输入文章内容"></el-input>             </el-form-item>             <el-form-item label="文章标签">                 <el-input v-model="tag" placeholder="请输入文章标签,按回车键添加标签"                     @keypress.enter.prevent="addArticleTag"></el-input>                 <div v-if="createForm.tagList.length">                     <el-tag v-for="(tag, index) in createForm.tagList" :key="tag + index" closable                         @close="delArticleTag(index)">                         {{ tag }}                     </el-tag>                 </div>             </el-form-item>         </el-form>         <template #footer>             <span class="dialog-footer">                 <el-button @click="handleClose">取消</el-button>                 <el-button type="primary" @click="handleConfirm">确定</el-button>             </span>         </template>     </el-dialog> </template> <style scoped lang="scss"> .pagination {     margin-top: 20px;     float: right } </style> 
测试 在侧边栏点击‘我的文章’,展示当前用户添加的文章
点击左上角‘添加文章’按钮,在弹出的编辑框中填写文章信息(添加标签的方法是填入一个标签后按回车键,再添加下一个),点击‘确定’,添加文章
可以看到刚才添加的文章
点击‘编辑’,编辑文章信息后点击‘确定’,修改文章信息
刚才修改的文章信息
点击‘删除’,删除对应的文章