admin 管理员组文章数量: 887021
项目源代码
gitee
github
基本分析
该案例前端采用Vue开发,后端采用Node.js开发。
将后台管理系统代码存放在admin
文件夹中,前端企业门户代码存放在web
文件夹中,server
文件夹存放服务器接口代码
admin相关部分
创建并配置文件
1.使用vue create admin
创建一个脚手架,并选择自定义配置一栏
- 选择vue3版本
- 对于路由器的模式选择默认模式,即hash模式
- 选择less选择器
- 选择将配置信息单独存放在一个文件夹中
创建相关文件即代码
Login.vue
和MainBox.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-plus
ui组件库和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 .value
的validate
方法中,该方法接收一个函数作为参数,并传入一个值,该值的作为就是作为手动验证的主要依据。
如下代码中,在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-text
和cancel-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-value
和inactive-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
- 为了操作方便,先将
req.body
中的属性结构出来 - 更新数据库中某条数据需要用到
_id
,正好该数据存放在token
中保存,可以解密token
取出_id
- 前端需要访问静态资源下的文件信息,所以需要拼接地址且不需要带上静态目录 /public
- 需要注意前端传递来的性别为字符串格式,需要转换类型
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 })
}
},
版权声明:本文标题:千锋--企业新闻发布管理项目笔记---上 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/jishu/1726802067h1031138.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论