admin 管理员组

文章数量: 887021

项目源代码

gitee
github

基本分析

该案例前端采用Vue开发,后端采用Node.js开发。
将后台管理系统代码存放在admin文件夹中,前端企业门户代码存放在web文件夹中,server文件夹存放服务器接口代码

admin相关部分

创建并配置文件

1.使用vue create admin创建一个脚手架,并选择自定义配置一栏

  1. 选择vue3版本

  1. 对于路由器的模式选择默认模式,即hash模式
  2. 选择less选择器
  3. 选择将配置信息单独存放在一个文件夹中

创建相关文件即代码

Login.vueMainBox.vue文件需要用到路由控制,所以对应的将两个文件存放在views文件夹中保管。
在路由文件夹下的index.js文件中添加如下代码,先保证路由可以正常匹配显示页面。
其中Login是用于登录验证的,MainBox是后台项目,在后台MainBox中,有多个模块需要使用到路由,且还需要通过权限控制路由,所以这里采用了动态添加路由实现嵌套路由

import { createRouter, createWebHashHistory } from 'vue-router'
// 导入路由匹配组件
import Login from '@/views/Login.vue'
import MainBox from '@/views/MainBox.vue'
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: '/login',
      component: Login
    },
    {
      path: '/mainbox',
      component: MainBox
    }
  ]
})

export default router

动态添加路由的方法router.addRoute()
views文件夹下新创建两个文件夹存放权限控制路由进行演示

嵌套路由概念:要将嵌套路由添加到现有的路由中,可以将路由的 name 作为第一个参数传递给 router.addRoute(),这将有效地添加路由,就像通过 children 添加的一样:

router.addRoute({ name: 'admin', path: '/admin', component: Admin })
router.addRoute('admin', { path: 'settings', component: AdminSettings })]
等价于下面的代码
router.addRoute({
  name: 'admin',
  path: '/admin',
  component: Admin,
  children: [{ path: 'settings', component: AdminSettings }],
})

以下是创建的代码实现嵌套路由关系

/* 注意path中添加'/'和不添加''的区别
  '/index':代表放在MainBox组件中显示,但是路径为http://xxx:8080/index
  'index':代表放在MainBox组件中显示,但是路径为http://xxx:8080/mainbox/index
*/
router.addRoute("MainBox", {
  path: 'index', //相对路径
  component: Home
})
router.addRoute("MainBox", {
  path: '/center',
  component: Center
})

但是在这里手动添加嵌套路由过于繁琐了,于是可以单独创建一个文件专门保管这些路径信息,之后在路由index.js文件中使用addRouter方法遍历添加即可。

import Home from '../views/home//Home.vue'
import Center from '../views/center/Center.vue'

/* 注意path中添加'/'和不添加''的区别
  '/index':代表放在MainBox组件中显示,但是路径为http://xxx:8080/index
  'index':代表放在MainBox组件中显示,但是路径为http://xxx:8080/mainbox/index
*/
const MyConfigRoutes = [
  {
    path: '/index',
    component: Home
  },
  {
    path: '/center',
    component: Center
  }
]
export default MyConfigRoutes

index.js文件中引入该嵌套路由信息,并循环遍历添加路由到mainbox组件中显示。

import MyConfigRoutes from './config.js'
--------
MyConfigRoutes.forEach(item => {
  router.addRoute('MainBox', item)
})

完善对应的views结构,将所有的文件创建好并安装上面嵌套路由的写法配置好嵌套路由

const MyConfigRoutes = [
  {
    path: '/index',
    component: Home
  },
  {
    path: '/center',
    component: Center
  },
  {
    path: '/user-manage/useradd',
    component: UserAdd
  },
  {
    path: '/user-manage/userlist',
    component: UserList
  },
  {
    path: '/news-manage/newsadd',
    component: NewsAdd
  },
  {
    path: '/news-manage/newslist',
    component: NewsList
  },
  {
    path: '/product-manage/productadd',
    component: ProductAdd
  },
  {
    path: '/product-manage/productlist',
    component: ProductLsit
  }
]

这个时候会出现一个问题,这些路由信息均是在mainbox中显示的内容,而这些内容直接可也在路径中访问显示,所以这个时候需要进行鉴权操作。即在遍历调用addRouter()前需要进行路由拦截判断操作。
基本代码如下

// 配置路由拦截
router.beforeEach((to, from, next) => {
  if (to.name === 'Login') {
    // 登录页面直接放行
    next()
  } else {
    if (!(localStorage.getItem('token'))) {
      // 没有token,直接重定向到登录页
      next('/login')
    } else {
      MyConfigRoutesApi() //封装了addRoute动态添加路由的方法
      next({
        path: to.fullPath //防止路由刚加载完成,没有识别到,再次进行跳转
      })
    }
  }
})

在这个代码片段中,不写 next()的原因是,执行MyConfigRoutesApi方法后,不能立即在next中就获取到当前页面的路径信息,所以会报错提示。
但是直接写成上面代码中的格式的时候又会产生一个问题,即死循环问题,在每次进入路由前都会配置嵌套路由,然后再次实现二次跳转,这是不对的,仅仅需要在第一次的时候实现该步骤即可。
所以怎么实现下面代码的要求是重要的。且这个第一次的变量是全局共享的,在每一个路由中都需要访问使用,这个时候就可以使用Vuex的相关知识了,将共享的数据配置在Vuex中的state中保存共享

	if (第一次) {
        MyConfigRoutesApi() //封装了addRoute动态添加路由的方法
        next({
          path: to.fullPath
        })
      } else {
        next()
      }

vuex文件中配置如下代码信息,其中state中的isGetAllRoutes变量就是全局共享的数据,所以路径都可以访问到它从而修改其值。

  state: {
    // 共享的数据,判断所以前天路由是否添加上
    isGetAllRoutes: false
  },
  mutations: {
    // 因为不经过ajax请求,所以直接使用commit方法
    changeGetAllRoute(state, value) {
      console.log(state, value)
      state.isGetAllRoutes = value
    }
  },

在路由index.js文件中修改代码如下,这样子就可以正常的显示内容了。

// 配置路由拦截
router.beforeEach((to, from, next) => {
  if (to.name === 'Login') {
    // 登录页面直接放行
    next()
  } else {
    if (!(localStorage.getItem('token'))) {
      // 没有token,直接重定向到登录页
      next('/login')
    } else {
      if (!store.state.isGetAllRoutes) { //状态为假,即第一次进入的时候会执行一次嵌套子路由的添加方法
        MyConfigRoutesApi() //封装了addRoute动态添加路由的方法
        next({
          path: to.fullPath
        })
      } else {
        next()
      }
    }
  }
})

const MyConfigRoutesApi = () => MyConfigRoutes.forEach(item => {
  router.addRoute('MainBox', item)
  store.commit("changeGetAllRoute", true) //调用commit方法触发changeGetAllRoute,从而修改状态
})

在项目中用到了element-plusui组件库和particles.vue3粒子效果库,均可查看对应的官网安装使用。
在引入成功后,给登录界面添加相应的粒子效果,直接复制想要的代码粘贴即可。
如果第particles.vue3粒子库无法实现效果可以使用另一个粒子库即vue-particles使用命令安装npm install vue-particles --save-dev
以下是使用ui组件库快速生成的form表单

    <div class="formContainer">
      <h1>企业登录</h1>
      <el-form
        ref="loginFormRef"
        :model="loginForm"
        status-icon
        :rules="loginRules"
        label-width="80px"
        class="demo-ruleForm"
      >
        <el-form-item label="用户名" prop="username">
          <el-input v-model="loginForm.username" autocomplete="off" />
        </el-form-item>
        <el-form-item label="密码" prop="password">
          <el-input
            v-model="loginForm.password"
            type="password"
            autocomplete="off"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="submitForm">登录</el-button>
        </el-form-item>
      </el-form>
    </div>

相应的js部分代码如下

export default {
  name: "Login",
  setup() {
    const loginFormRef = ref(); //表单的引用对象,用于校验
    const loginForm = reactive({
      username: "",
      password: "",
    }); //表单的数据对象
    const loginRules = reactive({
      username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
      password: [{ required: true, message: "请输入密码", trigger: "blur" }],
    }); //表单的校验,详细的配置

    function submitForm() {  
    }
    return {
      loginFormRef,
      loginForm,
      loginRules,
      submitForm,
    };
  },

每次点击的时候就会去触发submitForm方法,首先需要进行手动校验,校验的方法在loginFormRef .valuevalidate方法中,该方法接收一个函数作为参数,并传入一个值,该值的作为就是作为手动验证的主要依据。
如下代码中,在validate方法中,函数参数中的形参值value,该值的作用如下:当整个表单没有输入任何内容,即没有触发blur事件的情况下,直接点击按钮,该值为false。只要触发了blur事件并且输入了值,则该值为true。
在下面的代码中,如果用户输入了内容就直接将内容存放本地存储中,同时跳转到index页面。由于在setup方法中无法访问this.$router,因此需要引入组合式API,具体的push等方法和vue2一样

import { useRouter } from "vue-router";
-----------------------------------
setup(){
....................省略上诉重复代码
	const router = useRouter();
    function submitForm() {
      // 再次手动验证表单,防止用户没有输入内容触发事件直接点击提交按钮
      loginFormRef.value.validate((value) => {
        if (value) {
          //输入内容后,value为真则进行操作
          localStorage.setItem("token", "youtobich");
          // useRouter的作用相当于$router,进行编程式路由
          router.push("/index");
        }
      });
    }
}

设计mainbox区域

在该组件中引入该布局代码,为了方便后期管理,将其中不需要被路由管理的模块,封装为一个组件存放在component文件夹下,便于修改。

  <el-container>
    <el-aside width="200px">Aside</el-aside>
    <el-container>
      <el-header>Header</el-header>
      <el-main> <router-view></router-view></el-main>
    </el-container>
  </el-container>

SideMenu侧边栏区域设计

之后引入的结构如下,但是这样子原先的弹性盒子容器布局就会发生一点变化,这是因为我们采用组件的方式引入,打乱了原有的布局方式,这个时候官方给出的参数direction,子元素中有 el-header 或 el-footer 时为 vertical,否则为 horizontal

 <el-container>
    <SideMenu></SideMenu>
    <el-container direction="vertical">
      <TopHeader></TopHeader>
      <el-main> <router-view></router-view></el-main>
    </el-container>
  </el-container>

进入SideMenu组件中待见如下图所示的页面结构,这里也采用Menu菜单组件快速搭建

以下是一个基本的代码,需要注意的是,index的值必须是唯一的,用来控制区分哪一个模块被使用。
当使用UI组件库的icon图标的时候,也需要安装,使用npm命令安装npm install @element-plus/icons-vue
这里需要注意的是在该UI组件中,将icon图标封装成了一个一个组件,所以需要创建组件。代码如下

import {HomeFilled,Avatar,UserFilled,MessageBox,Reading,Pointer,} from "@element-plus/icons-vue";
export default {
  name: "SideMenu",
  components: { HomeFilled, Avatar, UserFilled, MessageBox, Reading, Pointer },
};

这个时候就可也在页面中使用了,这里扩展一个知识点,多个单词组成的大写,可也在结构中直接使用,也可也将全部的首字母转小写并以**-**连接使用。element plus中的字体图标是基于i标签生成的,且大多数组件名即为类名。可以控制样式

<el-icon><HomeFilled></HomeFilled></el-icon>
<el-icon><home-filled /></el-icon>
<template>
  <el-aside width="200px">
    <el-menu
      default-active="2"
      class="el-menu-vertical-demo"
      @open="handleOpen"
      @close="handleClose"
    >
      <!-- index必须是唯一标志,所以采取路径控制,因为每一个路径均是不同 -->
      <el-menu-item index="/index">
        <el-icon><HomeFilled></HomeFilled></el-icon>
        <span>首页</span>
      </el-menu-item>
      <el-menu-item index="/center">
        <el-icon><Avatar></Avatar></el-icon>
        <span>个人中心</span>
      </el-menu-item>
      <el-sub-menu index="/user-manage">
        <template #title>
          <el-icon><UserFilled /></el-icon>
          <span>用户管理</span>
        </template>
        <el-menu-item index="/user-manage/useradd">添加用户</el-menu-item>
        <el-menu-item index="/user-manage/userlist">用户列表</el-menu-item>
      </el-sub-menu>
      <el-sub-menu index="/news-manage">
        <template #title>
          <el-icon><MessageBox></MessageBox></el-icon>
          <span>新闻管理</span>
        </template>
        <el-menu-item index="/news-manage/newsadd">添加新闻</el-menu-item>
        <el-menu-item index="/news-manage/newslist">新闻列表</el-menu-item>
      </el-sub-menu>
      <el-sub-menu index="/product-manage">
        <template #title>
          <el-icon><Reading></Reading></el-icon>
          <span>产品管理</span>
        </template>
        <el-menu-item index="/product-manage/productadd">添加产品</el-menu-item>
        <el-menu-item index="/product-manage/productlist"
          >产品列表</el-menu-item
        >
      </el-sub-menu>
    </el-menu>
  </el-aside>
</template>

这个时候给侧边栏页面添加一个功能,点击header区域的按钮,折叠起来。
在menu组件身上有一个属性collapse,该属性传入一个布尔值,用来控制menu菜单栏是否折叠。(在折叠的时候需要将整个侧边栏的宽度从200px减少为64px,这里直接设置为auto,自动控制且有动画过渡)
因此为了实现兄弟组件之间传递数据,将控制折叠的布尔变量存放在vuex中保存。
以下是vuex中的数据情况,省略了之前的代码,只保留新添加的

  state: {
    // 默认为false,即不折叠
    isCollapse: false
  },
  mutations: {
    changeCollapse(state) {
      // 每次调用该方法的时候取反即可
      state.isCollapse = !state.isCollapse
    }
  },

因为这是vue3项目,且使用了setup函数,所以需要引入vuex形式的组合式api,代码如下

import { useStore } from "vuex";
export default {
  name: "SideMenu",
  components: { HomeFilled, Avatar, UserFilled, MessageBox, Reading, Pointer },
  setup() {
    const store = useStore(); //在组件中使用vuex
    return {
      store, //页面需要访问store.state中的数据
    };
  },
};

页面菜单中用到该属性的地方写法如下代码

:collapse="store.state.isCollapse"

但是代码写到这个的时候会出现一个问题,即每次点击刷新的时候,vuex中的状态都不会被保存,所以我们需要进行一个处理。
这个时候外卖引入了能帮助我们进行持久化操作的库,即vuex-persistedstate库。该库的作用是,可以控制一些状态值,使其在每次刷新的时候不会改变。
使用npm命令安装:npm install --save vuex-persistedstate
使用官方提供的代码,在vuex中使用插件

import { createStore } from "vuex";
import createPersistedState from "vuex-persistedstate";

const store = createStore({
  // ...
  plugins: [createPersistedState()],
});

这个时候保存状态的功能就已经实现了,每次启动都会将vuex中state中保存的所有属性保存到本地存储中,这样子就会将本不需要进行持久化的数据持久化,从而造成了代码的错误,程序流程错误。

因此我们需要指定哪些数据是需要持久化处理的。
在如下的代码中,我们在插件中指明了只需要持久化state中的isCollapse属性即可。

  state: {
    // 共享的数据,判断所有路由是否添加上
    isGetAllRoutes: false,
    // 默认为false,即不折叠
    isCollapse: false
  },
  plugins: [createPersistedState({
    paths: ["isCollapse"] //控制哪些属性是否持久化
  })],

侧边栏路由跳转即相关设置

当上面的代码写完后,这个时候页面就可以每次刷新保持侧边栏的状态了,这个时候需要给每一个侧边栏的模块添加路由跳转功能。
只需要在菜单栏中添加router属性即可,其作用:是否启用 vue-router 模式。 启用该模式会在激活导航时以 index 作为 path进行路由跳转 使用 default-active 来设置加载时的激活项。这个时候就是为什么之前的index值取路径的原因了。

<el-menu :collapse="store.state.isCollapse" :router="true">。。。</el-menu>

但是这个时候会出现一个问题,即点击侧边栏某个栏目的时候,该栏目会高亮显示,但每次刷新的时候,高亮就消失了,该如何处理。

这个时候需要用到菜单menu上的属性default-active,其作用就是在页面加载时默认激活菜单的 index(存放路径信息),配合router属性,控制当前路径高亮显示。
那么如何动态的获取当前所显示的路径信息,这个时候就需要用到vue-router身上的route属性了,该属性保存了完整的当前路径信息及其参数。可以使用该属性中的route.path获取当前的路径信息。

<el-menu :collapse="store.state.isCollapse" :router="true" :default-active="route.path">。。。</el-menu >
import { useRoute } from "vue-router";
---------------------------------------
setup() {
    const route = useRoute(); //使用路径,这里是route非router,route每个组件身上的路径各不相同
    return {
      route,
    };
},

推荐一个设置滚动条的设置

// 设置滚动条的样式
::-webkit-scrollbar {
设置滚动条的大小
  width: 5px;
  height: 5px;
  position: absolute;
}
::-webkit-scrollbar-thumb {
  // 滚动条上的滚动滑块的颜色
  background-color: #1890ff;
}
::-webkit-scrollbar-track {
  // 滚动条轨道的颜色
  background: #ddd;
}

TopHeader组件设计

设计一个左右盒子摆放,但盒子内部分别放入一个icon图标和span文字展示区域,之后引入下拉菜单组件库,在右侧图标上使用,使其点击触碰的时候会有下拉菜单显示。基本代码如下。
需要注意的是:凡是用到的icon组件图标,都需要引入并注册为组件标签使用

<template>
  <el-header>
    <div class="left">
      <el-icon><Menu /></el-icon>
      <span style="margin-left: 10px">企业门户网站管理系统</span>
    </div>
    <div class="right">
      <span style="margin-right: 10px">欢迎 某某 登录</span>
      <el-dropdown>
        <span class="el-dropdown-link">
          <el-icon :size="25"><User /></el-icon>
        </span>
        <template #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item>个人中心</el-dropdown-item>
            <el-dropdown-item>退出</el-dropdown-item>
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </div>
  </el-header>
</template>
设置图标和文字垂直居中显示技巧

left和right均为父盒子,均设置flex弹性盒子布局。在内部对图标i使用margin:auto实现和文字的摆方显示。

  .right,
  .left {
    display: flex;
    // 让内部字体图标和文字水平居中显示,所以的icon都是基于i标签
    i {
      margin: auto;
    }
  }
处理左右图标点击效果

vue3中组件上的事件,不写emit接收。默认当做原生事件,写了就当自定义事件处理
当点击左侧盒子的字体图标,控制侧边栏的展开和缩放,该方法在之前已经实现,所以只需要给该组件绑定方法即可。
当点击右侧盒中字体图标的时候,对其展开的内容进行路由控制,点击个人中心的时候实现路由跳转即可,点击退出的时候情况本地存储并实现跳转。
基本代码如下

//结构使用部分代码
<el-icon @click="handleCollapseState"><Menu /></el-icon>
<el-dropdown-item @click="handleRouterToCenter">个人中心</el-dropdown-item>
<el-dropdown-item @click="handleRouterLayout">退出</el-dropdown-item>
 --------------------------------------------------------
// 引入路由
import { useRouter } from "vue-router";
// 引入vuex
import { useStore } from "vuex";
--------------------------------------
 setup() {
    const router = useRouter();
    const store = useStore();
    // 每次调用修改折叠状态
    function handleCollapseState() {
      store.commit("changeCollapse");
    }
    // 跳转center
    function handleRouterToCenter() {
      router.push("/center");
    }
    // 退出功能
    function handleRouterLayout() {
      localStorage.removeItem("token");
      router.push("/login");
    }
    return {
      handleCollapseState,
      handleRouterToCenter,
      handleRouterLayout,
    };
  },

Topheader组件页面设计

在每次login页面登录成功的时候,可以将验证成功的用户信息返回,跳转到home页面,并且头部对用户信息的打印显示在头部区域。如图

像这种公共的数据部分,可以用户存储在vuex中保存。
在vuex中添加如下代码,同时将用户的信息持久化处理,防止每次刷新的时候丢失

  state: {
    // 存放用户信息
    userInfo: {}
  },
  mutations: {
    addUserInfo(state, value) {
      state.userInfo = value
    }
  },
  plugins: [createPersistedState({
   paths: ["isCollapse", "userInfo"] //控制哪些属性是否持久化
  })],

首先,在login初次登录成功的时候,在Controller中,对于login成功的时候返回的数据进行修改,代码如下,将用户信息返回给前端

      res.send({
        ActionType: 'OK',
        userInfoData: {
          username: result[0].username,
          gender: result[0].username ? result[0].username : 0, // 0代表保密
          introduction: result[0].introduction, //为空的情况下并不会返回个前端
          avatar: result[0].avatar,
          role: result[0].role
        }
      })

前端首次登录成功后,收到数据并将数据保存到vuex中。

 if (res.data.ActionType === "OK") {
    // 将用户数据保存到vuex中
    store.commit("addUserInfo", res.data.userInfoData);
    //跳转首页
    router.push("/index");
    }

在Topheader组件中,将用户名动态显示在顶部区域,可以使用vue3的计算属性将用户名先从vuex中取出来进行简化后再放在模板中。同时在退出登录的时候,需要删除vuex中的用户信息。

   TopHeader组件中的代码
   
	<span style="margin-right: 10px">欢迎 {{ username }} 登录</span>
	
	import { computed } from "vue";
    const username = computed(() => { //在return中返回
      return store.state.userInfo.username;
    });
     // 退出功能
    function handleRouterLayout() {
      localStorage.removeItem("token");
      // 清除vuex中的用户信息
      store.commit("clearUserInfo");
      router.push("/login");
    }
在vuex中的mutations中添加如下代码
    clearUserInfo(state) {
      state.userInfo = {}
    }

登录后,用户信息存放在本地存储中。

退出登录后删除vuex中的数据,本地存储中也随之删除了

home页面设计

home页面设计采用UI组件库生成,基本样式如下,顶部采用Page Header 页头组件生成,下面由两张卡片组成,所以引入了Card 卡片组件生成基本样式,在最后一个卡片中添加轮播图效果组件,即Carousel 走马灯组件。

基本代码如下

<template>
  <div>
    <el-page-header title="企业门户管理系统" icon="">
      <template #content>
        <span class="text-large font-600 mr-3"> 首页 </span>
      </template>
    </el-page-header>

    <el-card class="box-card"> </el-card>

    <el-card class="box-card">
      <template #header>
        <div class="card-header">
          <span>公司产品</span>
        </div>
      </template>
      <el-carousel :interval="4000" type="card" height="200px">
        <el-carousel-item v-for="item in 6" :key="item">
          <h3 text="2xl" justify="center">{{ item }}</h3>
        </el-carousel-item>
      </el-carousel>
    </el-card>
  </div>
</template>

设计第一个卡片布局的时候需要引入前提知识点如下

  • element plus的布局采用Layout布局,该布局将页面分为24份。
  • 通过 row 和 col 组件,并通过 col 组件的 span 属性我们就可以自由地组合布局。
  • 例如四个col。每一个span设计为8,则沾满全部,类似bootstrap的栅格布局

设计成如下图片所示,头像区域占四份,内容区域占20份。

代码如下

    <el-card class="box-card">
      <el-row>
        <el-col :span="4">头像区域</el-col>
        <el-col :span="20">内容区域</el-col>
      </el-row>
    </el-card>

修改主页代码,如图为最终结果,其头像根据用户是否上传头像,如果没有就以默认显示,后面的位置可以自定义设计。

    <el-card class="box-card">
      <el-row>
        <el-col :span="4"><el-avatar :size="100" :src="circleUrl" /></el-col>
        <el-col :span="20" style="line-height: 100px"
          >欢迎 {{ username }} 回来,{{ welcomText }}</el-col
        >
      </el-row>
    </el-card>
    -------------------------
    //setup函数中代码,需要引入computed api
    const store = useStore();
    const circleUrl = computed(() =>
      store.state.userInfo.avatar
        ? store.state.userInfo.avatar
        : "https://cube.elemecdn/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
    );
    const username = computed(() => store.state.userInfo.username);
    const welcomText = computed(() =>
      new Date().getHours() < 12 ? "再睡一会吧" : "喝一杯咖啡提提神吧"
    );

center页面布局

布局样式如图所示,也需要采用Layout布局使用,左边采用占8份,右边占16份分布。

先布局该页面的首部,和之前的home页面布局一样

添加如下代码后页面显示如上图所示

<el-page-header title="企业门户管理系统" icon="">
   <template #content>
      <span class="text-large font-600 mr-3"> 个人中心 </span>
   </template>
</el-page-header>

之后在下面主体部分采用Layout布局,<el-row></el-row>gutter属性规定了每一列之间的距离,直接填写数字,默认会转换为30px。之后添加两个 <el-col></el-col>添加两个列。并为列添加span属性,该属性规定了每一列占24份中的几份。

<el-row :gutter="30">
	 <el-col :span="8" class="el-col-left"></el-col>
	  <el-col :span="16"></el-col>
</el-row>

之后在每一列中添加Card 卡片组件
第一个列中添加卡片组件后,选择添加一个头像组件Avatar 头像代码如下所示。

<el-card class="box-card">
     <el-avatar :size="100" :src="circleUrl" />
     <h3>{{ username }}</h3>
     <h3>{{ role }}</h3>
</el-card>

第二列中添加卡片组件后,添加Card卡片组件添加一个标题,然后添加Form 表单组件,其中对应的参数可以参考官网选择。form表单中的数据,需要注册对应的ref和reactive值和设置验证规则

<el-card class="box-card">
       <template #header>
            <div class="card-header">
              <span>个人信息</span>
            </div>
       </template>
          <!-- 表单区域 -->
          <el-form
            ref="userFormRef"
            :model="userForm"
            :rules="userRules"
            label-width="120px"
            class="demo-ruleForm"
          >
            <el-form-item label="用户名" prop="username">
              <el-input v-model="userForm.username" />
            </el-form-item>

            <el-form-item label="性别" prop="gender">
              <el-select v-model="userForm.gender" style="width: 100%">
                <el-option
                  v-for="item in options"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
            <el-form-item label="个人简介" prop="introduction">
              <el-input
                resize="none"
                type="textarea"
                placeholder="请输入自我介绍"
                :rows="10"
                v-model="userForm.introduction"
              />
            </el-form-item>
            <el-form-item label="头像" prop="avatar">
              <el-upload
                class="avatar-uploader"
                action=""
                :show-file-list="false"
              >
                <img
                  v-if="userForm.avatar"
                  :src="userForm.avatar"
                  class="avatar"
                />
                <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
              </el-upload>
            </el-form-item>
          </el-form> 
</el-card>

完整的设计代码如下

<template>
  <div>
    <el-page-header title="企业门户管理系统" icon="">
      <template #content>
        <span class="text-large font-600 mr-3"> 个人中心 </span>
      </template>
    </el-page-header>

    <el-row :gutter="30">
      <el-col :span="8" class="el-col-left">
        <el-card class="box-card">
          <el-avatar :size="100" :src="circleUrl" />
          <h3>{{ username }}</h3>
          <h3>{{ role }}</h3>
        </el-card>
      </el-col>

      <el-col :span="16"
        ><el-card class="box-card">
          <template #header>
            <div class="card-header">
              <span>个人信息</span>
            </div>
          </template>
          <!-- 表单区域 -->
          <el-form
            ref="userFormRef"
            :model="userForm"
            :rules="userRules"
            label-width="120px"
            class="demo-ruleForm"
          >
            <el-form-item label="用户名" prop="username">
              <el-input v-model="userForm.username" />
            </el-form-item>

            <el-form-item label="性别" prop="gender">
              <el-select v-model="userForm.gender" style="width: 100%">
                <el-option
                  v-for="item in options"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
            <el-form-item label="个人简介" prop="introduction">
              <el-input
                resize="none"
                type="textarea"
                placeholder="请输入自我介绍"
                :rows="10"
                v-model="userForm.introduction"
              />
            </el-form-item>
            <el-form-item label="头像" prop="avatar">
              <el-upload
                class="avatar-uploader"
                action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
                :show-file-list="false"
                :auto-upload="false"
              >
                <img
                  v-if="userForm.avatar"
                  :src="userForm.avatar"
                  class="avatar"
                />
                <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
              </el-upload>
            </el-form-item>
          </el-form> </el-card
      ></el-col>
    </el-row>
  </div>
</template>

js部分代码如下

<script>
import { Plus } from "@element-plus/icons-vue";
import { useStore } from "vuex";
import { computed, ref, reactive } from "vue";
export default {
  name: "Center",
  components: { Plus },
  setup() {
    const store = useStore();
    const circleUrl = computed(() =>
      store.state.userInfo.avatar
        ? store.state.userInfo.avatar
        : "https://cube.elemecdn/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
    );
    const role = computed(() =>
      store.state.userInfo.role === 1 ? "管理员" : "编辑"
    );
    // 创建表单相关的数据
    // 结构store中的数据
    const { username, gender, introduction, avatar } = store.state.userInfo;

    const userFormRef = ref();
    const userForm = reactive({
      username,
      gender,
      introduction,
      avatar,
    });
    const userRules = reactive({
      username: [
        {
          required: true,
          message: "请输入用户名",
          trigger: "blur",
        },
      ],
      gender: [
        {
          required: true,
          message: "请输入性别",
          trigger: "blur",
        },
      ],
      introduction: [
        {
          required: true,
          message: "请输入自我介绍",
          trigger: "blur",
        },
      ],
      avatar: [
        {
          required: true,
          trigger: "blur",
        },
      ],
    });
    const options = [
      { label: "保密", value: 0 },
      { label: "男", value: 1 },
      { label: "女", value: 2 },
    ];
    return {
      circleUrl,
      username,
      role,
      userFormRef,
      userForm,
      userRules,
      options,
    };
  },
};
</script>

这个时候基本代码就完成,但是每次点击上传文件头像的时候,点完发现没有反应,这是因为我们没有在文件上传的时候作出相应的操作。

给文件上传绑定指定的组件API,:on-change该api无论文件上传成功还是失败都会去执行回调函数。

       <el-upload class="avatar-uploader"
            action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
            :show-file-list="false"
            :auto-upload="false"
            :on-change="handleChangeAvatar"
       ></el-upload>

在回调函数中会自动的接收一个文件参数信息,该参数是经过处理的,但是其中保留raw原生属性,可以将该属性转换为地址显示。使用URL.createObjectURL()创建一个文件路径,并将该路径赋予头像,即可显示。

    const handleChangeAvatar = (file) => {
      console.log(file);
      userForm.avatar = URL.createObjectURL(file.raw);
    };


这个时候就可以成功的显示图片了

之后就可以为该表单创建一个更新提交的按钮了,添加如下提交按钮代码。点击后执行回调函数,通过使用表单userFormRef中的validate方法验证所以表单信息是否验证通过。并打印提交的信息如图,但是发现,缺少用户上传的文件信息,即元素的文件,而不是这种处理后再本地显示的路径,所以需要给userForm添加一个字段file,当头像上传成功的时候,将文件的原生信息赋值过去。

<el-form-item>
       <el-button type="primary" @click="onSubmit">更新</el-button>
</el-form-item>
----------------------
    // 提交表单需要将表单信息提交到服务器
    const onSubmit = () => {
      // 所有表单验证通过
      userFormRef.value.validate((value) => {
        if (value) {
          console.log(userForm);
        }
      });
    };


handleChangeAvatar方法中添加如下代码,之后重新打印就可以发现文件的信息如图。

      userForm.file = file.raw; //转存原生文件信息


使用axios将数据发送给服务器,需要注意的是:因为涉及到文件上传服务器,所以需要使用FormData(),将表单数据一部分一部分的上传

    // 提交表单需要将表单信息提交到服务器
    const onSubmit = () => {
      // 所有表单验证通过
      userFormRef.value.validate((value) => {
        if (value) {
          console.log(userForm);
          const fm = new FormData();
          for (let i in userForm) {
            fm.append(i, userForm[i]);
          }
          axios({
            method: "POST",
            url: "/adminapi/user/upload",
            data: fm,
            headers: {
              "Content-Type": "multipart/form-data",
            },
          }).then((res) => {
            console.log(res.data);
          });
        }
      });
    };

可以在网络调试中发现,参数可以正常传递过去,现在只需要在服务器配置接口即可,于是在server文件中配置该路径信息。

获取更新后的数据并成功显示

在这里需要处理个人中心数据更新后,再次刷新显示的问题。
服务器返回的数据如下,需要每次将这些数据更新到vuex中保存,保证vuex中用户的信息一直是最新的。重点处理头像地址


在center中,当接收到服务器返回的数据的时候,就进行更新vuex的操作,将用户信息更改为最新的。
在axios的then回调中添加如下代码,更新vuex。会发现,当vuex中用户信息改变的时候,对应的本地存储中的数据和页面中的数据也会跟着改变。注意当前头像地址,将页面中用到该地址的地方都做统一修改

if (res.data.ActionType === "OK") {
     store.commit("addUserInfo", res.data.data);
}


打印结果如图,即如何将页面中用到avatar的地方进行处理。在后端,文件信息是存放在静态资源下的,所以需要使用该地址进行拼接访问http://localhost:3000' + circleUrl,忘记circleUrl是什么的看一下下面的代码,利用计算属性将store中存储的头像avatar信息取出来,即保存在后端的文件二进制数据。

    const circleUrl = computed(() =>
      store.state.userInfo.avatar
        ? store.state.userInfo.avatar
        : "https://cube.elemecdn/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
    );

这里是需要严重注意:在app.js文件中,由于之前对于后端访问静态资源没有做测试,所以使用后端页面访问静态资源的时候会报错,这是因为访问的时候,headers中没有authorization传递过来。所以执行的时候会报错。修改后端的部分代码如下,只需要对于后端访问的时候,没有token的情况直接放行,使后端可以正确访问静态资源即可

  if (token) {
	//代码和之前一致
  } else {
    next()
  }

这个时候http://localhost:3000' + circleUrl,就会去访问后端服务器静态资源下的资源,并成功显示图片了

但是依旧会有一个小bug,即文件上传图片,如果文件头像上传也使用这个办法那么就会出错,具体原因是每当文件头像上传的时候都会触发下面图片中的代码,注意其中使用URL.createObjectURL()方法的返回值赋值给了userForm.avatar,这个时候不点击提交按钮,页面是能够正常显示的,因为页面中头像使用的路径是以blob开头的。但是一旦点击了提交按钮,那么userForm.avatar就会改变,这个时候头像文件的显示就会出错,因为找不到该路径,所以需要进行处理。



利用计算属性 :src="avatarUpload"修改代码如下

   // 显示文件上传图片的头像
    const avatarUpload = computed(() => {
      return userForm.avatar.includes("blob")? userForm.avatar: "http://localhost:3000" + userForm.avatar;
    });

代码写到这,问题只剩一个,即用户信息中,原本存在头像信息和个人信息,但是用户只点击了个人信息修改,不修改头像,那么当点击更新的时候,命名头像文件的表单验证都通过了,为什么控制台还报错?

出问题的代码片段如下,即userForm.file出问题了,因为这个file属性不像其他属性一样,存放在vuex中进行了持久化处理,而是一个新值,即每次刷新后,该值都是null。恰巧这个时候更新的时候原本有图片信息,于是就不会区触发handleChangeAvatar方法将文件的原生信息赋值给file属性。这就会导致为null的原因。这个时候将一个空的file属性上传给服务器,而服务器在后端处理的时候,由于file取出来为空,取一个空属性的filename属性就会报错。所以需要对于后端代码进行修改。



后端修改代码如下
userController文件中修改代码如下

//对avatar进行判断性操作,也为了后期返回值方便操作
const avatar = req.file ? `/avataruploads/${req.file.filename}` : ''
    if (avatar) {
      res.send({
        ActionType: 'OK',
        data: {
          username,
          gender: Number(gender),
          introduction,
          avatar //返回服务器静态资源下的头像路径
        }
      })
    } else {
      res.send({
        ActionType: 'OK',
        data: {
          // 不更新上一次的头像数据
          username,
          gender: Number(gender),
          introduction,
        }
      })
    }

在userServices中修改代码如下

    if (avatar) {
      return UserModel.updateOne({ _id }, {
        username, gender, instroduction, avatar
      })
    } else {
      return UserModel.updateOne({ _id }, {
        // 不更新上一次的头像数据
        username, gender, instroduction
      })
    }

在vuex中对于更新userInfo的方法进行修改

    addUserInfo(state, value) {
      // 为了旧值不更新的情况和新值进行合并
      state.userInfo = { ...state.userInfo, ...value }
    },

这样子当不选择图片的时候,程序可以正常运行(如果重新登录的时候,个人简介信息没有,那是在useServices文件中解构的时候将introduction拼错了,导致数据库没有更新)

封装center组件

由于center组件中需要处理的事情很多,导致代码很繁琐,维护起来很困难所以需要进行封装。
首先将axios部分提取到util文件下,将axios相关的都封装在一起。
在util文件下添加upload文件

import axios from "axios"

function upload(path, userForm) {
  const fm = new FormData();
  for (let i in userForm) {
    fm.append(i, userForm[i]);
  }
  return axios({
    method: "POST",
    url: path,
    data: fm,
    headers: {
      "Content-Type": "multipart/form-data",
    },
  }).then(res => res.data)
}
export default upload

在原先center中使用到该方法地方进行替换,这里使用async awati方法将接收的axios的promise的值使用。

      userFormRef.value.validate(async (value) => {
        if (value) {
          let res = await upload("/adminapi/user/upload", userForm);

          if (res.ActionType === "OK") {
            store.commit("addUserInfo", res.data);
            ElMessage({
              message: "修改成功.",
              type: "success",
            });
          }
        } else {
          ElMessage.error("请重新操作.");
        }
      });

之后开始封装结构中的upload上传部分,由于该组件没有设计到路由,所以在component文件夹下创建一个Upload.vue文件保存封装的代码,这里最重要的是如何传递数据,因为设计的是父传子,所以采用最简单的props传递,注意vue3如何使用props即可。且在子组件中如何将文件信息提交给父组件上传服务器,采用自定义事件

 父组件使用
 <!-- 封装upload上传组件 -->
<upload :avatar="userForm.avatar" @handleChangeAvatar="handleChangeAvatar"></upload>

    // 处理头像上传文件改变的时候函数,将上传的文件显示在框内
    const handleChangeAvatar = (file) => { //此时的file就是原生信息
      userForm.avatar = URL.createObjectURL(file);
      userForm.file = file; //转存原生文件信息
    };
//Upload.vue组件
<template>
  <el-upload
    class="avatar-uploader"
    action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
    :show-file-list="false"
    :auto-upload="false"
    :on-change="handleChangeAvatar"
  >
    <img v-if="avatar" :src="avatarUpload" class="avatar" />
    <el-icon v-else class="avatar-uploader-icon">
      <Plus />
    </el-icon>
  </el-upload>
</template>

<script>
import { computed } from "vue";
import { Plus } from "@element-plus/icons-vue";
export default {
  name: "Upload",
  components: { Plus },
  props: ["avatar"],
  emit: ["handleChangeAvatar"],
  setup(props, ctx) {
    // 显示文件上传图片的头像
    const avatarUpload = computed(() => {
      return props.avatar.includes("blob")
        ? props.avatar
        : "http://localhost:3000" + props.avatar;
    });
    // 处理头像上传文件改变的时候函数,将上传的文件显示在框内
    const handleChangeAvatar = (file) => {
      // 触发事件,传递参数
      ctx.emit("handleChangeAvatar", file.raw);
    };
    return { handleChangeAvatar, avatarUpload };
  },
};
</script>

用户管理

用户添加

页面效果如下,基本代码和center中的基本一致。

前端中使用封装过的upload文件进行axios的发送请求,代码如下,前端配置完成后就在服务器处理该接口的请求即可。

    const onSubmit = () => {
      userFormRef.value.validate(async (value) => {
        if (value) {
          // 发送ajax请求
          await upload("/adminapi/user/add", userForm);
          // 添加成功跳转
          router.push("/user-manage/userlist");
        }
      });
    };
用户列表

当添加用户页面写完后,就需要处理用户列表页面了。结构布局采用UI组件库中的table组件创建
需要注意的是,在表格中底层借助了for信息生成多列数据,需要在创建列的时候指定好props的值归属即可。只不过在这里我们需要借助ajax发送网络请求获取响应式的数据即可。

<template>
  <div>
    <el-page-header icon="" title="用户管理">
      <template #content>
        <span class="text-large font-600 mr-3"> 用户列表 </span>
      </template>
    </el-page-header>
    <el-table :data="tableData" border style="width: 100%">
      <el-table-column prop="date" label="Date" width="180" />
      <el-table-column prop="name" label="Name" width="180" />
      <el-table-column prop="address" label="Address" />
    </el-table>
  </div>
</template>



setup函数中,将表格的数据设置为响应式,并且设置挂载完成时的生命周期钩子去调用方法获取列表数据

    const tableData = ref([]);
    const getTableData = async () => {
      const res = await axios.get("/adminapi/user/list");
      console.log(res.data);
    };
    onMounted(() => {
      // 发送请求获取数据
      getTableData();
    });

对于后端处理返回的数据进行处理,将数据赋值给表格对象

显示用户名区域

<el-table-column prop="username" label="用户名" />

显示头像区域,这里需要注意:scope接收的参数可以理解为tableData.value的值,而其中的row参数可以理解为数组中的每一个对象,这样子scope.row.avatar就可以获取到每一个用户的头像信息

<el-table-column label="头像">
        <template #default="scope">
          <div v-if="scope.row.avatar">
            <el-avatar
              :size="50"
              :src="'http://localhost:3000' + scope.row.avatar"
            />
          </div>
          <div v-else>
            <el-avatar
              :size="50"
              src="https://cube.elemecdn/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
            />
          </div>
        </template>
</el-table-column>

权限区域

<el-table-column label="角色">
        <template #default="scope">
          <el-tag v-if="scope.row.role === 2" class="ml-2" type="success"
            >编辑</el-tag
          >
          <el-tag v-else class="ml-2" type="danger">管理员</el-tag>
        </template>
      </el-table-column>

操作区域,值得注意的是:两个按钮在点击后需要触发不同的事件,而在事件中传递的scope.$index, scope.row分别为当前操作对象在数组中的索引和值

     <el-table-column label="操作">
        <template #default="scope">
          <el-button size="small" @click="handleEdit(scope.$index, scope.row)"
            >编辑</el-button
          >
          <el-button
            size="small"
            type="danger"
            @click="handleDelete(scope.$index, scope.row)"
            >删除</el-button
          >
        </template>
      </el-table-column>


基本页面图如下

删除功能

这个时候需要针对操作部分进行美观,希望点击删除的时候,弹出一个提示功能,于是借助Popconfirm 气泡确认框UI库。将按钮包裹在该组件中的插槽中。其中confirm-button-textcancel-button-text分别为点击按钮时候弹出的文字修饰,而confirm为点击确定时候需要执行的回调函数。不要将事件触发给直接绑定在按钮,否则不点击确定也会触发

          <el-popconfirm
            title="确定删除吗?"
            confirm-button-text="确定"
            cancel-button-text="取消"
            @confirm="handleDelete(scope.$index, scope.row)"
          >
            <template #reference>
              <el-button size="small" type="danger">删除</el-button>
            </template>
          </el-popconfirm>

之后就可以在点击确定的时候,在回调函数中发送ajax请求,将需要删除数据的id传递过去即可。更新完成后再次显示最新的内容即可
这里采用RESTful的设计风格,即路径一致,但http的请求方式不同。在后端配置请求路由

    const handleDelete = async (index, row) => {
      await axios.delete(`/adminapi/user/list/${row._id}`);
      // 每次删除后更新当前页面即可
      getTableData();
      // 放置删除成功消息反馈
      ElMessage({message: "删除成功.",type: "success",});
    };
编辑功能

当点击编辑按钮的时候,利用Dialog 对话框创建一个修改对话框。如图

在下面的Dialog 对话框中,dialogVisible属性用于控制该编辑弹窗是否显示,默认情况下是false,当用户点击编辑按钮的时候该属性的值在回调函数中被修改为true,这个时候页面显示,无论点击弹窗页面中任意按钮,均会将弹窗页面隐藏掉。

  <el-dialog v-model="dialogVisible" title="编辑弹窗" width="30%">
  //表格部分的代码,和添加用户的一致,删除部分代码即可
          <template #footer>
          <span class="dialog-footer">
            <el-button @click="dialogVisible = false">取消</el-button>
            <el-button type="primary" @click="dialogVisible = false">
              确定
            </el-button>
          </span>
        </template>
</el-dialog>

这个时候需要添加一个功能,即点击编辑按钮的同时,当页面弹窗的时候,里面的内容会显示出来,想修改哪一个就修改哪一个。(由于这里没有事先请求的时候返回密码项,所以可以再次请求一次获取需要的数据)

//因为大部分代码都不变,所以这里简略
const result = await axios.get(`/adminapi/user/list/${row._id}`);
router.get('/adminapi/user/list/:id', UserController.getList)
const result = await UserServices.getList(req.params) //这里的req.params要么空要么有值为id的属性
async ({ id }) => { 
    // 若id不存在,查找空对象则返回全部数据
    return id ? UserModel.find({ _id: id }, [...]) : UserModel.find({}, [...])
  },

之后就是对于响应式userForm表单如何处理了,由于我们设置的是reactive响应式的,所以直接将属性赋值过去的话,会丢失响应式,因此我们采用Object.assign()合并两个对象内容实现浅拷贝。reactive最外层的内存地址改变的时候可以监测到。(如果是ref定义的数据,则无该问题)
在点击编辑按钮的回调函数中,将请求回的数据使用Object.assign()合并到userForm中。

    const handleEdit = async (index, row) => {
      // 可以再次封装获取需要的数据
      const result = await axios.get(`/adminapi/user/list/${row._id}`);
      Object.assign(userForm, ...result.data.data);
      dialogVisible.value = true;
    };


当点击编辑弹窗的时候页面能够显示正确的内容后,就需要对确定按钮进行回调函数的处理。在该回调函数中,进行了一次网络请求,将本次修改的数据信息发送给后端更新。

    const handleEditConfirm = () => {
      // 验证表单数据是否通过
      userFormRef.value.validate(async (value) => {
        if (value) {
          // 发送修改的数据取后台更新
          await axios.put(`/adminapi/user/list/${userForm._id}`, userForm);
          // 将窗口设置为不可见
          dialogVisible.value = false;
          // 更新最新的数据列表
          getTableData();
        }
      });
    };
用户权限管理

即对于非管理员的用户不展示用户管理列表,不允许对用户的数据机进行修改操作。
在用户管理的侧边栏组件上添加一个自定义指令v-isShow

<el-sub-menu index="/user-manage" v-isShow></el-sub-menu index>

可以根据官网选择自定义指令的写法。这里采用配置directives属性的方式完。在该属性中配置自定义指令,该指令中注意vue3中提供的钩子函数和vue2不一样,这里提供了mounted钩子,在绑定元素的父组件及他自己的所有子节点都挂载完成后调用。其中el形参用于获取当前DOM节点。使用原生方法移除该DOM即可。其中需要获取当前登录用户的选项,即需要获取vuex中的数据信息,于是第二个参数binding就提供了该功能。binding.instance该属性用于获取该指令组件的实例,组件实例上提供了vuex的数据,还提供了router的数据。

  directives: {
    isShow: {
      mounted(el, binding) {
        const { role } = binding.instance.store.state.userInfo;
        if (role === 2) {
          el.parentNode.removeChild(el, binding);
        }
      },
    },
  },

但是这样子有一个bug如图,非管理员的用户直接输入地址查看的时候,可以直接将需要的页面显示出来,这是不对的,所以需要进行前端的路由拦截处理。

在前端路由中对于这两个路径进行权限校验,设置校验的变量isAuth

修改路由index文件中的代码

const MyConfigRoutesApi = () => MyConfigRoutes.forEach(item => {
  // 每次进入路由,都进行判断是不是管理员和user开头的路径
  checkPermission(item) && router.addRoute('MainBox', item)
  store.commit("changeGetAllRoute", true)
})
const checkPermission = (item) => {
  if (item.meta?.isAuth) { //只有user起头的路径需要校验
    return store.state.userInfo.role === 1 //管理员直接放行
  }
  // 不需要校验直接放行,非user-manage开头的路径不需要进行验证
  return true
}

但是这么改完存在一个严重的bug,即当一个管理员登录的时候,可以正常访问用户管理模块,且在登录的时候将isGetAllRoutes属性改为真了。该属性是vuex中的一个共享的数据。如果这个时候退出该管理员的账号,且不刷新页面,那么这个时候的isGetAllRoutes一直为真,非管理员用户登录的时候,进入路由拦截的时候不会进入验证阶段。

这里采用在login页面中,一旦登录成功,就将该值该为假,以便下一次进行路由判断

store.commit("changeGetAllRoute", false);

但是这么修改完还是存在问题,第一个问题依旧和之前的一样,即登录完管理员账号退出,不刷新的前提下登录非管理员账号依旧可以访问user路径的信息还有一个问题是每一次addRoute都会不断的向router中添加路径,导致很多重复的内容,导致原先管理员的信息没有被覆盖。所以需要进行操作,在每次进入的时候删除原有的路径

checkPermission(item) && router.addRoute('MainBox', item)

以下是登录管理员后切换非管理员的图

以下是修改后的代码

      if (!store.state.isGetAllRoutes) {
        // 每次进入删除旧的路径,父路径删除子路径也没了
        router.removeRoute('MainBox')
        MyConfigRoutesApi() //封装了addRoute动态添加路由的方法
        next({
          path: to.fullPath
        })
      } else {
        next()
      }
    }
    -----------------------------
  //在router.addRoute('MainBox', item)之前执行
  // 清空旧的MainBox路径,创建新的
  if (!router.hasRoute('MainBox')) {
    router.addRoute({
      name: 'MainBox',
      path: '/mainbox',
      component: MainBox
    })
  }

运行解释:当管理员初次登录的时候,isGetAllRoutes变量false不变,并且服务器将token返回,浏览器保存token。当访问的是非login的页面的时候,就进行token验证,验证成功的就行路径的添加步骤,根据isGetAllRoutes决定是否添加路径操作。登录后将该值设置为假,则需要进入路径添加阶段,在路径添加阶段之前,先进入router.removeRoute('MainBox'),将原先旧的路径全部删除,然后进入MyConfigRoutesApi方法中,如果没有MainBox该父组件的时候就添加。在遍历的过程中,checkPermission方法会进校验,即非user页面直接添加到路由中,是user的页面会进行身份校验,是管理员就添加进去。**所以这个时候router.addRoute中存放了user页面的相关路径信息。这个时候退出登录不刷新页面的。这些路径信息是存在的。**如果这个时候登录一个非管理员账户,如果不进行之前的removeRoute操作清除之前的路径信息,那么addRoute。就是不断的往里面添加,存在管理员的路径信息,这个时候直接输入路径,完全可以进入管理员账户存储的路径信息。所以要对之前的路径进行删除。

新闻管理

添加新闻

在新闻添加中,唯一的重点部分就是内容的添加处理,这里需要使用到富文本编辑器wangeditor。且这是一个新的模型,对应的在数据库中要创建一个新闻模型。
在添加新闻中,大部分代码是和之前一致的,也是采用表单的形式,这里就不添加重复的代码,只针对表单的数据类型进行描述

    const newsFormRef = ref();
    const newsForm = reactive({
      title: "", //标题
      content: "", //内容
      category: 1, //新闻的种类 1最新动态 2典型案例 3 通知公告
      cover: "", //图片
      file: null,
      isPublish: 0, //发布标志位
    });
    const newsRules = reactive({
      title: [{ required: true, message: "请输入标题", trigger: "blur" }],
      content: [{ required: true, message: "请输入内容", trigger: "blur" }],
      category: [{ required: true, message: "请输入类别", trigger: "blur" }],
      cover: [{ required: true, message: "请上传头像", trigger: "blur" }],
    });

在页面中正常添加普通的表单项,当添加到内容模块的时候需要注意,需要使用到富文本编辑器的功能,于是就去wangeditor中参照官方文档进行操作。
由于在添加新闻和修改新闻的时候都会使用到富文本编辑,所以将该组件单独封装使用。在component文件下创建一个editor.vue存放。
使用npm i wangeditor --save命令安装富文本编辑器的库。

import E from 'wangeditor'
const editor = new E('#div1')
// 或者 const editor = new E( document.getElementById('div1') )
editor.create()

Edior.vue文件中创建如下代码,注意这里需要注意获取DOM节点的时机,需要在页面挂载完成的时候才插入富文本,所以将关键的富文本代码放入onMounted函数中。这个时候在引入该组件的位置使用组件标签创建实例,就可以看见该富文本编辑的效果了。

<template>
  <div id="editor"></div>
</template>

<script>
import { onMounted } from "vue";
import E from "wangeditor";
export default {
  name: "Editor",
  setup() {
    onMounted(() => {
      const editor = new E("#editor");
      editor.create();
    });

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


那么这个子组件中的输入的内容如何传递给父组件使用,这是一个存在的问题,需要处理。
在wangeditor官方文档中,给出了对应的回调函数onchange(),用户操作(鼠标点击、键盘打字等)导致的内容变化之后,会自动触发 onchange 函数执行。具体代码直接从官网复制即可,需要注意的是富文本编辑器的基本思想就是:在文本框内给文字添加css样式形参一个HTML代码片段

  emit: ["handleEditorEvent"],
  setup(props, ctx) {
    onMounted(() => {
      const editor = new E("#editor");
      editor.create();
      // 配置 onchange 回调函数
      editor.config.onchange = function (newHtml) {
        ctx.emit("handleEditorEvent", newHtml);
      };
    });
 <Editor @handleEditorEvent="handleEditorEvent"></Editor>
 -------------
// 富文本编辑的自定义事件
const handleEditorEvent = (value) => {
   console.log(value);
   newsForm.content = value;
};


将所有的富文本数据收集后,将newsForm发送给后端处理。

    const submitForm = () => {
      newsFormRef.value.validate(async (value) => {
        if (value) {
          await upload("/adminapi/news/add", newsForm);
          router.push("/news-manage/newslist");
        }
      });
    };
新闻列表

基本布局和用户列表一致,采用Card卡片作为背景板,使用table组件进行展示内容。

    const tableData = ref([]);
    onMounted(async () => {
      const res = await axios.get("/adminapi/nes/list");
      tableData.value = res.data.data;
    });

这是switch开关的代码,需要根据isPublish的值动态绑定开关的状态,其中v-model严格绑定布尔值,所以对于数值无效,因此这里借助了active-valueinactive-value完成,

<el-table-column label="是否发布">
      <template #default="scope">
            <el-switch
              v-model="scope.row.isPublish"
              :active-value="1"
              :inactive-value="0"
          />
     </template>
</el-table-column>

其余代码基本和之前一样省略。
页面最终效果如下,这里会用到其他第三方库对时间进行处理,这里使用的是dayjs库。

switch开关发布状态更改

每次修改完按钮的状态都需要传递给服务器更新数据库,依次保存最新的数据。

    // 新闻发布回调
    const handleIsPublish = async (item) => {
      await axios.put(`/adminapi/news/publish/${item._id}`, {
        isPublish: item.isPublish,
      });
      // 重新更新
      getTableList();
      ElMessage({
        message: "修改成功.",
        type: "success",
      });
    };
新闻预览

基本效果如下

给第一个编辑预览按钮绑定事件,处理预览的功能,需要预览对应的新闻,需要将当前项传递过去scope.row。并且在结构中插入弹窗显示的dialog组件,其中divider是分割线组件。由于是弹窗组件,所以需要设置弹窗组件显示和隐藏的变量

@click="handlePreview(scope.row)"

    <!-- 弹窗显示组件 -->
    <el-dialog v-model="dialogVisible" title="编辑预览" width="45%">
      <h1>{{ previewDate.title }}</h1>
      <h5 style="color: #ccc">
        {{ formatTime.formatCditTime(previewDate.editTime) }}
      </h5>
      <el-divider>
        <el-icon><star-filled /></el-icon>
      </el-divider>
      <div v-html="previewDate.content" class="haveImg"></div>
    </el-dialog>
    // 预览数据,采用ref解决对象赋值的问题
    const previewDate = ref({});
    //弹窗状态控制变量
    const dialogVisible = ref(false);
    // 预览回调
    const handlePreview = (item) => {
      previewDate.value = item;
      dialogVisible.value = true;
    };
新闻删除

Popconfirm 气泡确认框中,confirm是点击确认的时候才会执行的回调

  @confirm="handleDelete(scope.row)"
    // 删除确认回调
    const handleDelete = async (item) => {
      await axios.delete(`/adminapi/news/list/${item._id}`);
      getTableList(); //重新获取最新数据
    };
新闻编辑

在新闻编辑中不采用之前用户编辑时候的弹窗更改信息了,而是创建一个组件用于修改新闻信息,当用户点击编辑新闻的时候,就会跳转过去并显示新闻的内容供用户修改。

    // 新闻编辑,按钮的事件回调,携带需要修改的新闻id过去
    const handleEdit = (item) => {
      router.push(`/news-manager/editnews/${item._id}`);
    };
//前端路由配置中添加给路由信息
  {
    path: '/news-manager/editnews/:id',
    component: NewsEdit
  },

NewsEdit组件的基本信息可以复用之前的新闻添加组件,也可以对于新闻添加组件进行封装,将内容部分提取出来单独创建一个vue文件,提高复用性。
给表头绑定跳转功能,icon默认为箭头显示。点击的时候触发回调函数,回到上一个页面

<el-page-header title="新闻编辑" @back="goBack"></el-page-header>

const goBack = () => {
   router.back();
};

一进入该页面就立即获取该页面对应的新闻信息,可以复用之前的/adminapi/news/list接口,将数据显示出来

// 获取编辑时候传递过来id新闻的数据
onMounted(async () => {
      const res = await axios.get(`/adminapi/news/list/${route.params.id}`);
      Object.assign(newsForm, res.data.data[0]);
});

但是这么写完会出现一个问题,就是富文本区域没有更新,这个时候就将获取的content数据传递给富文本组件更新了。

<Editor @handleEditorEvent="handleEditorEvent" :content="newsForm.content"></Editor>

在富文本编辑组件中设置props属性接收,并根据官方文档editor.txt.html()设置文本区内容。

//在onMounted方法中添加如下代码
// 设置初始值
props.content && editor.txt.html(props.content);

但是这么写会出现一个问题,就是在NewsEdit组件中,onMounted是异步执行的,而页面是直接创建完成的,即Editor组件在页面中创建完成了,于是Editor组件的onMounted方法就执行了,后期NewsEdit组件获取到数据后再次跳转后,Editor组件不会执行更新富文本区域的内容了。所以采用一个简单的办法就是使用v-if延迟创建Editor组件的时机

<Editor
     v-if="newsForm.content"
     @handleEditorEvent="handleEditorEvent"
     :content="newsForm.content"
></Editor>

当修改完成上面的代码的时候,就可以修改原先的表单按钮提交数据了,因为涉及到图片文件上传,所以复用了原先的函数,且在数据上传后跳转到上一页面

    const submitForm = () => {
      newsFormRef.value.validate(async (value) => {
        if (value) {
          // 设计图片文件上传,使用封装的组件
          await upload("/adminapi/news/list", newsForm);
          router.back(); //回退上一个页面
        }
      });
    };

以下是更新数据后返回上一级显示更新情况

产品管理

添加产品

创建对应的产品列表结构,和原先的基本一致,给出表单的约束内容等,其余修改即可

    const productFormRef = ref();
    const productForm = reactive({
      title: "",
      introduction: "",
      detail: "",
      cover: "",
      file: null,
    });
    const productRules = reactive({
      title: [{ required: true, message: "请输入标题", trigger: "blur" }],
      introduction: [
        { required: true, message: "请输入产品介绍", trigger: "blur" },
      ],
      detail: [{ required: true, message: "请输入细节描述", trigger: "blur" }],
      cover: [{ required: true, message: "请选择封面", trigger: "blur" }],
    });

在提交按钮中,修改添加新闻的路由信息

    const onSubmit = () => {
      productFormRef.value.validate(async (value) => {
        if (value) {
          await upload("/adminapi/product/add", productForm);
          router.push("/product-manage/productlist");
        }
      });
    };

效果图如下

产品列表
删除产品
    // 删除确认回调
    const handleDelete = async (item) => {
      await axios.delete(`/adminapi/product/list/${item._id}`);
      await getTableList();
    };
编辑产品

代码和之前的新闻编辑类似,修改即可。配置产品的路由信息
一进入该页面,就立即获取指定id的产品数据显示,因为在后端已经封装好了接口所以直接使用

    onMounted(() => {
      getData();
    });
    const getData = async () => {
      const res = await axios.get(`/adminapi/product/list/${route.params.id}`);
      Object.assign(productForm, ...res.data.data);
    };

当编辑内容完成后需要更新数据库,在数据更新后跳转值列表页查看最新的数据

    const onSubmit = () => {
      productFormRef.value.validate(async (value) => {
        if (value) {
          await upload("/adminapi/product/list", productForm);
          router.push("/product-manage/productlist");
        }
      });
    };

产品数据和home轮播图联动

 <el-carousel :interval="4000" type="card" height="200px" v-if="loopList.length">
        <el-carousel-item v-for="item in loopList" :key="item._id">
          <div
            :style="{
              backgroundImage: `url(http://localhost:3000${item.cover})`,
              backgroundSize: 'cover',
            }"
          >
            <h3 text="2xl" justify="center">{{ item.title }}</h3>
          </div>
        </el-carousel-item>
</el-carousel>
	const loopList = ref([])
    onMounted(async () => {
      getTableList();
    });
    const getTableList = async () => {
      const res = await axios.get("/adminapi/product/list");
      loopList.value = res.data.data;
    };

server相关

使用exprees生成器快速生成一个骨架。
全局安装生成器后使用:express serve命令快速生成一个基于express的文件。
进入该文件后使用:npm i 安装该包的依赖项。

 <el-switch 省略代码和之前一样 @change="handleIsPublish(scope.row)"/>
     
     // 新闻发布回调
    const handleIsPublish = async (item) => {
      console.log(item._id, item.isPublish);
      await axios.put(`/adminapi/news/publish/${item._id}`, {
        isPublish: item.isPublish,
      });
      // 重新更新,封装获取列表数据
      getTableList();  
      ElMessage({
        message: "修改成功.",
        type: "success",
      });
    };

vue与node互连

尝试让vue项目和后端node.js互连,这里通过框架生成的/users路径进行测验,但是这里一定会发送跨域问题,即vue的端口为8080,而express端口号为3000.所以需要解决。
vue中,在login登录页面中,在点击登录的时候,发送网络请求给后端。

// 尝试和后端进行交互
axios.get("http://localhost:8080/users").then((res) => {
       console.log(res.data);
});

如果出现以下情况,就是被浏览器的同源策略处理了,出现跨域问题,这个时候可以使用vue提供的反向代理了。在vue的脚手架中提供了解决的方案。

vue.config.js文件中配置反向代理,即让vue充当服务器与后台服务器连接,从而实现同源策略的解决。
基本代码如下
需要注意的是,这里匹配到/users路径后,默认情况下,会将该路径拼接在target属性后发送给目标服务器处理。

  devServer: {
    proxy: {
      '/users': {
        target: 'http://localhost:3000',
        changeOrigin: true
      }
    }
  }

目标服务器接收到请求后进行路径匹配,发现/users路径匹配成功,于是进入usersRouter路由模块处理,由于没有任何路径跟在/users/后所以直接给前端返回send里的内容。

目标服务器中的中间件处理
app.use('/users', usersRouter);
-----------------------------------
//userRouter文件中
router.get('/', function (req, res, next) {
  res.send('respond with a resource');
});

设计MVC结构

因为我们需要一套admin和web展示页两套路由,所以需要创建两种匹配方式。每一个模块都需要使用MVC架构。

设计Models文件夹

该文件存放mongodb数据库相关的信息,使用mongoose库限制语法。
安装:npm i mongoose
在Models文件夹中创建UserModel.js文件,存放用户的信息约束,代码如下

const mongoose = require('mongoose')
const Schema = mongoose.Schema

// 定义约束
const UserType = {
  username: String,
  password: String,
  gender: Number, //性别 0=男,1=女
  introduction: String, //介绍
  avatar: String, //头像
  role: Number //区别管理员或普通用户
}

const UserModel = mongoose.model('user', new Schema(UserType))

module.exports = UserModel

先创建User用户相关的文件,在每一个MVC中的admin文件夹中创建,

在UserRouter.js文件中创建如下代码。负责匹配前端请求的路径信息,然后进入控制层,在控制层中完成与模型层的交互。

const express = require('express')
const router = express.Router()
const UserController = require('../../controller/admin/UserController.js')

// 只负责路由控制,逻辑代码交给控制才能
router.post('/adminapi/user/login', UserController.login)

module.exports = router

在UserController.js文件中创建如下代码,在controller层,不与数据库直接交互,将交互处理数据的任务再次传递给模型层处理。在查询数据库返回的结果中,使用find查询,返回的结果是一个数组,如果数组长度为0就代码数据库无该数据信息,返回报错信息。数据库的查询操作涉及异步,所以使用async await控制,防止没查询到结果就直接往下执行代码

const UserServices = require('../../services/admin/UserServices.js')

const UserController = {
  login: async (req, res) => {
    console.log(req.body)
    const result = await UserServices.login(req.body)
    if (result.length === 0) {
      res.send({
        code: '-1',
        error: '用户名或密码错误'
      })
    } else {
      res.send({
        ActionType: 'OK'
      })
    }
  }
}

module.exports = UserController

在UserServices.js文件中代码如下,在该文件中控制如何与数据库进行交互。使用数据库模型UserModel 进行约束查询

const UserModel = require('../../models/UserModel.js')

const UserServices = {
  // 数据库查询设计异步操作
  login: async ({ username, passwrod }) => {
    // 返回一个数组
    return UserModel.find({
      username, passwrod
    })
  }
}

module.exports = UserServices

在app.js主文件中引入如下代码

var UserRouter = require('./routes/admin/UserRouter.js')
//创建路由中间,凡是没匹配到的路径都进入该路由文件中执行操作
app.use(UserRouter)

连接mongodb数据库

创建如下代码结构,在db.config.js文件中存放连接数据库的代码

const mongoose = require('mongoose')

// 连接数据库,并创建数据库名,其集合名为users
mongoose.connect('mongodb://127.0.0.1:27017/company-system')

在www入口文件中添加如下代码引入数据库:require('../config/db.config.js')

这个时候使用数据库可视化工具查看,是否添加上新数据库

在前端admin文件的登录页面中修改发送axios请求的路径和参数,其中loginForm是表单收集的username和password组成的reactive对象

// 尝试和后端进行交互
 axios.post("/adminapi/user/login", loginForm).then((res) => {
       console.log(res.data);
});

在数据库中事先插入一条管理员数据admin用户,在登录页面输入成功后,控制台给出成功的返回信息

JWT模块

这里先完善之前发送ajax后的代码,引入UI组件库中的消息反馈组件。
当输入的用户信息不正确时,会弹出消息提示用户。,且需要在输入信息正确前实现JWT的存储。

  import { ElMessage } from "element-plus";
--------------------------
   function submitForm() {
      // 再次手动验证表单,防止用户没有输入内容触发事件直接点击提交按钮
      loginFormRef.value.validate((value) => {
        if (value) {
          // 尝试和后端进行交互
          axios.post("/adminapi/user/login", loginForm).then((res) => {
            console.log(res.data);
            if (res.data.ActionType === "OK") {
              //输入内容后,value为真则进行操作
              localStorage.setItem("token", "youtobich");
              // useRouter的作用相当于$router,进行编程式路由
              router.push("/index");
            } else {
              ElMessage.error("用户名或密码错误.");
            }
          });
        }
      });
    }


每次前后端访问。不论什么页面,都需要经过token的处理,验证token是否正确,所以这个时候需要前后端都设置统一处理的步骤。前端可以采用axios拦截器,后端采用路由中间件进行统一拦截判断。

后端路由统一拦截验证

后端是生成token的地方,如何生成token,需要安装:npm i jsonwebtoken库。单独创建一个util文件夹存放公共的jwt模块。
加密token和解密token的代码如下:

const jsonwebtoken = require('jsonwebtoken')
// 创建一个密钥
const secret = '我是你爷爷'
// 二次封装
const jwt = {
  sign(value, time) {
    return jsonwebtoken.sign(value, secret, { expiresIn: time })
  },
  verify(token) {
    try {
      return jsonwebtoken.verify(token, secret)
    } catch {
      return false
    }
  }
}
module.exports = jwt

在UserController.js文件中引入该代码,当第一次登录查询数据库的时候,如果成功会给前端返回正确的信息,在返回信息前,生成一个token并通过请求头发送给前端。
其中最重要的是向前端发送header头部信息: res.header("Authorization", token),其中Authorization是http字段,存放用户验证信息字段。

      const jwt = require('../../util/JWT.js')
      。。。。。。。。。。。。。。。。。。。。。
      // 代表验证通过,初次设置token
      const token = jwt.sign({
        _id: result[0]._id,
        username: result[0].username
      }, '10s')
      // 给前端请求头发送token
      res.header("Authorization", token)
      res.send({
        ActionType: 'OK'
      })

前端在登录的网络请求中可以查看到后端生成的token,通过header传递过来。这个时候前端需要借助拦截器,统一处理请求。

生成一个util文件在admin文件夹中配置axios拦截器代码。
在main.js文件中引入该文件,让axios的拦截器工作

在该文件中配置如下基本代码。
负责请求的时候将token发送给服务器验证和服务器返回的时候将token保存。需要注意的是jwt规定了携带Authorization的时候需要添加上Bearer 字段后面带上token传递给服务器。

// 配置拦截器
import axios from 'axios'

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
  // 每次请求服务器都会取出token发送出去
  const token = localStorage.getItem('token')
  config.headers.Authorization = `Bearer ${token}`
  return config;
}, function (error) {

  return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
  // 每次接收都会取token存储
  const { authorization } = response.headers
  authorization && localStorage.setItem('token', authorization)
  return response;
}, function (error) {

  return Promise.reject(error);
});

在app.js文件中进行统一路由处理判断token

// 添加token统一处理
app.use((req, res, next) => {
  // 如果为login登录页面直接放行,初次登录无token值
  if (req.url === '/adminapi/user/login') {
    next()
    return
  }
  // 如果存在token需要进行判断处理
  // 取出token   'Bearer tokenValue'
  const token = req.headers['authorization'].split(' ')[1]
  if (token) {
    // 存在token,进行解密验证
    const payload = jwt.verify(token) //验证失败返回false
    if (payload) {
      // 再次进行加密传递给前端,存放新的token和时效
      //防止活动窗口的时候token时效报错。
      const newToken = jwt.sign({
        _id: payload._id,
        username: payload.username
      }, '10s')
      // 将新token发送给前端,被拦截器拦截保存新token
      res.header('Authorization', newToken)
      next()
    } else {
      //错误返回前端错误码
      res.status(401).send({ errorInfo: 'token失效' })
    }
  }
})

这个时候前端每次刷新token都会是新的存储,一旦超过了token的时效就会返回401错误,所以需要在前端进行处理。

这个时候可也才axios拦截器中的响应拦截器中添加如下代码,在返回错误信息的时候,走第二个函数处理,实现跳转

axios.interceptors.response.use(function (response) {
  // 每次接收都会取token存储
  const { authorization } = response.headers
  authorization && localStorage.setItem('token', authorization)
  return response;
}, function (error) {
  if (error.request.status === 401) {
    // 重定向到login主页
    location.href = '/#/login' //会跳转到http://localhost:8080/#/login 注意不带/#的情况和这个不一样
  }
  return Promise.reject(error);
});

这个时候一个基本的登录验证token就实现了。

个人信息上传即文件路径处理

由于使用express,所以无法对文件上传作出处理,即req.body无法获取上传的文件信息组成的对象,为空。所以需要借助一个库解决。
安装:npm install --save multer

const multer  = require('multer')
const upload = multer({ dest: 'uploads/' })
每次经过该路由的时候,都先经过multer的处理,其中avatar是前端传递来的文件参数名,需要保持前后端一致
app.post('/profile', upload.single('avatar'), function (req, res, next) {
  // req.file 是 `avatar` 文件的信息
  // req.body 将具有文本域数据,如果存在的话
})

在代码中引入该库,并使用,注意需要修改文件信息保持的路径,这里将信息保持到静态资源下,便于前端使用

const multer = require('multer')
const upload = multer({ dest: './public/avataruploads/' })

router.post('/adminapi/user/upload', upload.single('file'), UserController.upload)

如图,如果存在文件信息,那么req.file就会打印第二个对象,该对象就保存了文件的详细信息参数,其中filename就是保存在静态资源下的路径。

先设计Controller中的upload

  1. 为了操作方便,先将req.body中的属性结构出来
  2. 更新数据库中某条数据需要用到_id,正好该数据存放在token中保存,可以解密token取出_id
  3. 前端需要访问静态资源下的文件信息,所以需要拼接地址且不需要带上静态目录 /public
  4. 需要注意前端传递来的性别为字符串格式,需要转换类型
  upload: async (req, res) => {
    const { username, gender, introduction } = req.body
    // 头token中解密出id,只要能到这里就代表之前的token验证成功,所以这里不需要验证步骤
    const token = req.headers['authorization'].split(' ')[1]
    const payload = jwt.verify(token)
    // 拼接文件路径
    const avatar = `/avataruploads/${req.file.filename}`
    await UserServices.upload({
      _id: payload._id,
      username,
      gender: Number(gender),
      introduction,
      avatar
    })
    res.send({
      ActionType: 'OK'
    })
  }

在UserServices中设计upload代码如下,使用UserModel.updateOne()方法查找第一个符合条件的数据,其中第一个参数为查找的条件,后面为更新的数据。这里需要return返回,否则数据库无法成功更新数据。

  upload: async ({ _id, username, gender, instroduction, avatar }) => {
    return UserModel.updateOne({ _id }, {
      username, gender, instroduction, avatar
    })
  }

最终结果如图所示

但是这样子写完代码后会存在一个问题,即用户修改信息配置且成功上传到数据库更新了,但是用户每次刷新了页面,数据就会丢失。所以还需要跳转到前端center页面中进行处理。需要首先在后端将最新的数据返回给前端,即用户输入的数据更新后再次返回即可。这里重点需要注意的是头像路由在前端如何处理。接下来,跳转到center页面继续编辑代码

    res.send({
      ActionType: 'OK',
      data: {
        username,
        gender: Number(gender),
        introduction,
        avatar //返回服务器静态资源下的头像路径
      }
    })

用户管理

添加新的路由中间件在UserRouter中处理。需要注意,上传新数据也使用到了文件,所以需要使用multer库协助获取文件部分的信息。

router.post('/adminapi/user/add', upload.single('file'), UserController.add)

在UserController中配置该方法。由于是添加信息,所以对于身份等信息的验证并不在意,所以只需要将数据提交给模型层直接插入到数据库即可。

  add: async (req, res) => {
    const { username, password, role, introduction, gender } = req.body
    const avatar = req.file ? `/avataruploads/${req.file.filename}` : ''
    await UserServices.add({ username, password, role: Number(role), introduction, gender, avatar })
    res.send({
      ActionType: 'OK'
    })
  }

在UserServices中添加新的方法

  add: async ({ username, password, role, introduction, gender, avatar }) => {
    return UserModel.create({ username, password, role, introduction, gender, avatar })
  }
用户列表

在路由中间件中添加如下代码

router.get('/adminapi/user/list', UserController.getList)

在对应的Controller层中添加如下代码

  getList: async (req, res) => {
    const result = await UserServices.getList()
    res.send({
      ActionType: 'OK',
      data: result
    })
  }

在UserServices中代码如下,查询特定的属性列按如下方法

  getList: async () => {
    return UserModel.find({}, ['username', 'gender', 'introduction', 'avatar', 'role'])
  }
删除功能

使用动态路由,每次接收不同的id值

//RESTful的设计风格,即路径一致,但http的请求方式不同
router.delete('/adminapi/user/list/:id', UserController.deleteList)

配置Controller中的代码,其中,动态参数通过req.params获取,将删除的id号给下一层

  deleteList: async (req, res) => {
    await UserServices.deleteList({ _id: req.params.id })
    res.send({
      ActionType: 'OK'
    })
  }

配置UserServices中的代码,使用deleteOne删除第一个符合条件的数据

  deleteList: async (id) => {
    return UserModel.deleteOne({ _id: id })
  }
编辑功能

编辑功能中的确认更新路由

router.put('/adminapi/user/list/:id', UserController.putList)

UserController中配置

  putList: async (req, res) => {
    await UserServices.putList({ ...req.body, ...req.params })
    res.send({
      ActionType: 'OK'
    })
  },

UserServices中代码

  putList: async ({ username, password, role, introduction, gender, id }) => {
    return UserModel.updateOne({ _id: id }, { username, password, role, introduction, gender })
  },

新闻管理

添加新闻

后端代码和之前的差不多所以这里简单的给出代码

//创建NewsModel.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema

const NewsType = {
  title: String,
  content: String,
  category: Number, //类别1 2 3
  cover: String, //存文件信息
  isPublish: Number, //未发布 已发布
  editTime: Date //编辑的时间
}

const NewsModel = mongoose.model('news', new Schema(NewsType))

module.exports = NewsModel

//创建NewsRouter.js
const express = require('express')
const router = express.Router()
const multer = require('multer')
const upload = multer({ dest: 'public/newsuploads/' })
const NewsController = require('../../controller/admin/NewsController.js')

router.post('/adminapi/news/add', upload.single('file'), NewsController.add)

module.exports = router

//创建NewsController.js
const NewsServices = require("../../services/admin/NewsServices")

const NewsController = {
  add: async (req, res) => {
    const { title, content, category, isPublish } = req.body
    const cover = req.file ? `/newsuploads/${req.file.filename}` : ''
    await NewsServices.add({
      title,
      content,
      category: Number(category),
      cover,
      isPublish: Number(isPublish),
      editTime: new Date() //额外传递创建的时间
    })
    res.send({
      ActionType: 'OK'
    })
  }
}

module.exports = NewsController

//创建NewsServices
const NewsModel = require('../../models/NewsModel.js')

const NewsServices = {
  add: async ({ title, content, category, cover, isPublish, editTime }) => {
    return NewsModel.create({ title, content, category, cover, isPublish, editTime })
  }
}

module.exports = NewsServices

前端输入完数据,数据库成功存入信息

新闻列表

添加新闻代码如下,在前端收到数据后进行存储,显示表格

//NewsRouter
router.get('/adminapi/nes/list', NewsController.getList)
//NewsController
getList: async (req, res) => {
    const result = await NewsServices.getList()
    console.log(result)
    res.send({
      ActionType: 'OK',
      data: result
    })
//NewsServices
getList: async () => {
    return NewsModel.find({})
  }
switch开关状态修改
router.put('/adminapi/news/publish/:id', NewsController.publish)
publish: async (req, res) => {
    await NewsServices.publish({ ...req.params, ...req.body })
    res.send({
      ActionType: 'OK'
    })
}
publish: async ({ id, isPublish }) => {
    return NewsModel.updateOne({ _id: id }, { isPublish })
}
新闻删除
router.delete('/adminapi/news/list/:id', NewsController.deleteList)

deleteList: async (req, res) => {
    await NewsServices.deleteList(req.params.id)
    res.send({
      ActionType: 'OK'
    })
  }
  
deleteList: async (id) => {
    return NewsModel.deleteOne({ _id: id })
}
新闻编辑

获取列表数据显示

//复用路径,进入同一个NewsController的方法中处理
router.get('/adminapi/news/list', NewsController.getList)
router.get('/adminapi/news/list/:id', NewsController.getList)
//区别,携带id信息
const result = await NewsServices.getList({ _id: req.params.id })

getList: async ({ _id }) => {
    //有id查找指定的,无id查找全部
    return _id ? NewsModel.find({ _id }) : NewsModel.find({})
},

更新列表数据

router.post('/adminapi/news/list', upload.single('file'), NewsController.updateList)

  updateList: async (req, res) => {
    const { title, content, category, isPublish, _id } = req.body
    const cover = req.file ? `/newsuploads/${req.file.filename}` : '' //头像可能不会再次更新
    await NewsServices.updateList({
      _id,
      title,
      content,
      category: Number(category),
      isPublish: Number(isPublish),
      editTime: new Date(),
      cover
    })
    res.send({
      ActionType: 'OK'
    })
  },
  
  updateList: async ({ _id, title, content, category, isPublish, editTime, cover }) => {
    if (cover) { //对头像进行判断
      return NewsModel.updateOne({ _id }, { title, content, category, isPublish, editTime, cover })
    } else {
      return NewsModel.updateOne({ _id }, { title, content, category, isPublish, editTime })
    }
  },

产品管理

添加产品

代码和之前基本一致

//模型设计
const mongoose = require('mongoose')
const Schema = mongoose.Schema

const ProductType = {
  title: String,
  introduction: String,
  detail: String,
  cover: String,
  editTime: Date
}

const ProductModel = mongoose.model('products', new Schema(ProductType))

module.exports = ProductModel

//路由
router.post('/adminapi/product/add', upload.single('file'), ProductController.add)

//ProductController
  add: async (req, res) => {
    const { title, introduction, detail } = req.body
    const cover = req.file ? `/productuploads/${req.file.filename}` : ''
    await ProductServices.add({ title, introduction, detail, cover, editTime: new Date() })
    res.send({
      ActionType: 'OK'
    })
  },

//ProductServices
  add: async ({ title, introduction, detail, cover, editTime }) => {
    return ProductModel.create({ title, introduction, detail, cover, editTime })
  },

产品列表

删除产品
router.delete('/adminapi/product/list/:id', ProductController.deleteList)

deleteList: async (req, res) => {
    await ProductServices.deleteList({ _id: req.params.id })
    res.send({
      ActionType: 'OK'
    })
}

deleteList: async (id) => {
    return ProductModel.deleteOne({ _id: id },)
}
编辑产品
//产品列表的获取数据
router.get('/adminapi/product/list', ProductController.getList)
//获取指定产品的数据
router.get('/adminapi/product/list/:id', ProductController.getList)

  getList: async (req, res) => {
    const result = await ProductServices.getList(req.params)
    res.send({
      ActionType: 'OK',
      data: result
    })
  },
  
  getList: async ({ id }) => {
    return id ? ProductModel.find({ _id: id }) : ProductModel.find({})
  },

更新编辑的产品信息

router.post('/adminapi/product/list', upload.single('file'), ProductController.updateList)

  updateList: async (req, res) => {
    const { title, introduction, detail, _id } = req.body
    const cover = req.file ? `/productuploads/${req.file.filename}` : ''
    await ProductServices.updateList({
      _id,
      title,
      introduction,
      detail,
      editTime: new Date(),
      cover
    })
    res.send({
      ActionType: 'OK'
    })
  },
  
  updateList: async ({ _id, title, introduction, detail, cover, editTime }) => {
    if (cover) {
      return ProductModel.updateOne({ _id }, { _id, title, introduction, detail, cover, editTime })
    } else {
      return ProductModel.updateOne({ _id }, { _id, title, introduction, detail, editTime })
    }
  },

本文标签: 新闻发布 笔记 项目 企业