图书商城项目练习①管理后台Vue2/ElementUI

  • 图书商城项目练习①管理后台Vue2/ElementUI已关闭评论
  • 187 次浏览
  • A+
所属分类:Web前端
摘要

本系列文章是为学习Vue的项目练习笔记,尽量详细记录一下一个完整项目的开发过程。面向初学者,本人也是初学者,搬砖技术还不成熟。项目在技术上前端为主,包含一些后端代码,从基础的数据库(Sqlite)、到后端服务Node.js(Express),再到Web端的Vue,包含服务端、管理后台、商城网站、小程序/App,分为下面多个篇文档。

图书商城项目练习①管理后台Vue2/ElementUI

本系列文章是为学习Vue的项目练习笔记,尽量详细记录一下一个完整项目的开发过程。面向初学者,本人也是初学者,搬砖技术还不成熟。项目在技术上前端为主,包含一些后端代码,从基础的数据库(Sqlite)、到后端服务Node.js(Express),再到Web端的Vue,包含服务端、管理后台、商城网站、小程序/App,分为下面多个篇文档。

?系列目录


00、管理后台Vue2+ElementUI

这是一个比较典型的管理后台练习项目,包含登录、框架页、导航路由、导航标签、数据管理、字典管理等基础功能。管理后台的业务大多是数据管理CRUD功能,该项目只是是简单实现了几个模块。同时针对CRUD,整理了一个模板?template。

?技术路线

  • Vue v2.*
  • ElementUI v2.*

?相关组件

  • vuex:状态管理
  • vue-router:前端路由
  • axios:HTTP调用
  • echarts:图表组件,按需定制
  • i18n:多语言国际化vue-i18nv8.*版本
  • @wangeditor:富文本编辑器
  • Less:CSS预处理器/语言

?源代码地址Github / KWebNoteGitee / KWebNote,管理后台代码在目录?book_admin下。

?在线体验地址??http://kanding.gitee.io/kwebnote (任意用户名、密码。通过gitee静态页面Gitee Pages部署的,所以这里部署的版本是写了个mock模拟api,路由用的hash模式)。

图书商城项目练习①管理后台Vue2/ElementUI


01、创建项目/准备

创建图书管理后台项目“book_admin”,基于@vue/cli,通过其vue ui管理工具,可视化操作创建项目。

图书商城项目练习①管理后台Vue2/ElementUI

  • 选择项目目录,开始创建项目。
  • 可以选择内置的多种预设模式,也可选择“手动”模式,按需设置项目的详细规则。

图书商城项目练习①管理后台Vue2/ElementUI

vue创建的项目已经包含了一个基础的架子了,如下图,主html页面文件“index.html”,入口JS文件“main.js”,入口Vue文件“App.vue”。

图书商城项目练习①管理后台Vue2/ElementUI


02、主页面/框架页面

管理后端是SPA单页应用,创建主框架页面“Main.vue”,登录后的所有内容都在这个主页面内呈现和管理。页面视图关系如下图:

图书商城项目练习①管理后台Vue2/ElementUI

2.1、Main.vue

因此,主页面就比较重要,是搬砖的基座,实际效果和布局结构图如下:

图书商城项目练习①管理后台Vue2/ElementUI

  • Header:头部区域,包含Logo、标签栏(存放打开的页面,类似浏览器的多页签)、系统按钮(最右侧区域)。
  • 路由菜单:系统导航菜单,数据来自路由配置信息。
  • 面包屑:内容区域当前视图的路由信息。
  • 内容区域:当前视图内容呈现区域。
    • 缓存<keep-alive>,配合多标签组件视图缓存,切换标签后视图的状态会被保持。
    • 切换动画<transition>,切换内容时的动画效果。
    • 内容滚动处理:内容区域的高度、宽度自适应铺满,如果内容超出该区域,则显示滚动条,不会导致整个页面出现滚动条。
  • Footer:底部区域,好像也没啥用,先放这吧!
<template>   <el-container style="height:100%">     <el-container class="main-aside">       <!-- 左侧 :logo+导航菜单 -->       <el-aside :width="config.menuCollapse?'auto':'200px'">         <MenuSidebar />       </el-aside>        <!-- 右侧 :头部+主内容-->       <el-container>         <!-- 头部 -->         <el-header :style="config.thema" class="header">           <!-- 标签工具栏 -->           <div style="flex:1;overflow:hidden">             <TabsBar ref="tabsBar"></TabsBar>           </div>         	<!-- 右侧的系统操作按钮 -->           <i class="el-icon-setting h-button" v-on:click="$refs.userConfig.show()" title="系统设置"></i>           <el-dropdown class="header-userbox" @command="handleCommand">             <span>               <img :src="$api.URL.proxy+'/file/f1.jpg'" alt="头像" />               [ {{$store.state.user.name}} ]               <i class="el-icon-arrow-down el-icon--right" style="font-size:12px"></i>             </span>             <el-dropdown-menu slot="dropdown">               <el-dropdown-item command="pwd">修改密码</el-dropdown-item>               <el-dropdown-item command="about">                 <i class="el-icon-info"></i>关于               </el-dropdown-item>               <el-dropdown-item command="user">                 <i class="el-icon-user-solid"></i>个人中心               </el-dropdown-item>               <el-divider></el-divider>               <el-dropdown-item command="logout" icon="el-icon-circle-close">退出登录</el-dropdown-item>             </el-dropdown-menu>           </el-dropdown>         </el-header>         <!-- 主内容区域 -->         <el-main class="main-wrapper">           <!-- 面包屑 -->           <div class="breadcrumb-bar">             <el-button type="text"               :icon="config.menuCollapse?'el-icon-s-unfold':'el-icon-s-fold'"               v-on:click="config.menuCollapse=!config.menuCollapse" ></el-button>             <el-breadcrumb separator="/" style="display:inlne-block">               <el-breadcrumb-item v-for="r in $route.matched" :key="r.name">{{r.meta?.lang ? $t('menu.' + r.meta.lang) : r.meta?.title}}</el-breadcrumb-item>             </el-breadcrumb>           </div>           <!-- 页面内容的容器 -->           <div class="main view-scroll">             <!-- 加了切换动画、保存页面状态 -->             <transition :name="config.routerAnimation?'fade-transform':''" mode="out-in">               <keep-alive :include="cacheNames">                 <router-view></router-view>               </keep-alive>             </transition>           </div>         </el-main>       </el-container>     </el-container>     <!-- footer -->     <el-footer height="30px">{{$consts.footer}}</el-footer>     <UserConfig ref="userConfig"></UserConfig>   </el-container> </template> 

2.2、导航菜单&路由

每个后台系统都会有导航菜单,支持多级展示、可收缩,效果如下图:

图书商城项目练习①管理后台Vue2/ElementUI

?‍♂️实现过程

1、路由的配置:这里用的是本地路由(vue-router中的路由配置信息),实际项目中路由可后台管理,或者本地+后台结合。本地路由配置“routes.js”详见Github / KWebNote

import constants from '@/assets/constants' import Vue from 'vue' import VueRouter from 'vue-router' //路由配置 import baseRoutes from './routes' //注册路由插件 Vue.use(VueRouter) // 创建路由 const router = new VueRouter({   mode: 'history',           //模式   base: process.env.BASE_URL,   routes: baseRoutes,        //路由配置 })  // 路由全局守卫-导航前,登录token判断 router.beforeEach((to, from, next) => {   if (to.path === '/login')     return next();   // 除开登录页面,其他页面都验证token,如果没有token则跳转到登录页面   const token = sessionStorage.getItem('admin_token');   if (!token)     return next('/login');   else     next(); }) router.afterEach((to, from) => {   //更新网页标题   document.title = constants.sysName + '-' + to.meta.title; }) 

?404页面的配置:路由的匹配是从上而下的,404页面路由放到最后即可,然后路径使用通配符匹配所有地址,{ path: '*', component: 404 }

2、导航菜单组件:创建“MenuSidebar.vue”,用<el-menu>组件显示多级菜单,启用路由导航router,菜单的数据就是来自前面的路由。

3、树形菜单:导航菜单项-递归,菜单项用一个“MenuItem.vue”组件来实现递归路由树,如果路由还有子节点children,则递归调用组件自身。

<template>   <el-menu-item v-if="!hasChildren" :index="item.path">     <i :class="item.meta.icon"></i>     <!-- 名称用title插槽 -->     <span slot="title">{{title(item)}}</span>   </el-menu-item>   <el-submenu v-else :index="item.path">     <template slot="title">       <i :class="item.meta.icon"></i>       <span slot="title">{{title(item)}}</span>     </template>     <MenuItem v-for="child in children" :item="child" :key="child.path"></MenuItem>   </el-submenu> </template> <script> export default {   name: 'MenuItem',   props: ['item'],   computed: {     children() {       return this.item?.children?.filter(s => !s.hidden);     },     hasChildren() {       return this.item?.children?.length > 0;     },   },   methods: {     title(item) {       return item.meta?.lang ? this.$t('menu.' + item.meta.lang) : item.meta?.title;     }   } } </script> 

2.3、配置路由动画

就是在路由切换页面视图的时候,有一个过渡动画效果,如下图。

图书商城项目练习①管理后台Vue2/ElementUI

主要是使用Vue的<transition>组件来实现过渡动画,设置其name和对应CSS动画即可,可参考另外一篇《Vue2快速上门(2)-模板语法 / Vue动画》

<transition :name="config.routerAnimation?'fade-transform':''" mode="out-in">   <keep-alive :include="cacheNames">     <router-view></router-view>   </keep-alive> </transition>    <style> // 路由切换动画 /* fade-transform */ .fade-transform-leave-active, .fade-transform-enter-active {   transition: all 0.5s; } .fade-transform-enter {   opacity: 0;   transform: translateX(30px); } .fade-transform-leave-to {   opacity: 0;   transform: translateX(-30px); } </style> 

?注意动画模式mode="out-in",避免布局变更引起的显示异常。name值是用于动画CSS样式的类名,这里的的name用了一个用户配置属性,目的是可以配置是否开启路由转场动画。

2.4、多标签工具栏

如下效果图,类似Chrome浏览器的多标签效果,打开的路由视图标题显示在标签栏,可以刷新、关闭、切换。切换时会保留视图状态,这样就可以很方便的多标签操作了。

图书商城项目练习①管理后台Vue2/ElementUI

?方案设计:

图书商城项目练习①管理后台Vue2/ElementUI

  • 监测路由变化,记录打开的页面路由,保存在vuex的store中。同时记录打开的页面名称,用于<keep-alive>include实现定向路由缓存,过滤不需要缓存的组件,也是实现视图刷新的关键。
  • 显示缓存的路由列表,就是看到的标签栏。
  • 关闭按钮,激活的路由视图显示关闭按钮,关闭后自动路由到下一个页面/标签。
  • 固定的标签:对于如“主页”的路由固定在标签栏(会在路由信息中配置),在初始化的时候就显示在标签栏,固定的标签不支持关闭。
  • 右键菜单功能:刷新、关闭、关闭其他、关闭所有,更新缓存的路由列表。

?‍♂️实现过程:

1、在vuex中添加一个子模块“tabBars.js”,单独管理标签的状态信息。提供缓存路由列表的操作方法:添加、删除、删除其他、删除所有、清空。

 export default {   namespaced: true,   state: {     cacheRoutes: [], //缓存的路由,用于标签栏使用     cacheNames: [],  //缓存的打开的路由Name,用于Keep-alive的缓存白名单   },   mutations: {     add(state, obj) {       if (!state.cacheRoutes.some(s => s.path === obj.path)) {         //添加打开的路由,只需要path、name、mata         state.cacheRoutes.push({ path: obj.path, name: obj.name, meta: obj.meta });         state.cacheNames = state.cacheRoutes.map(s => s.name);       }     },     remove(state, obj) {       const i = state.cacheRoutes.findIndex(s => s.path === obj.path);       if (i < 0)         return;       state.cacheRoutes.splice(i, 1);       state.cacheNames = state.cacheRoutes.map(s => s.name);     },     removeName(state, obj) {       //只移除缓存名字       const i = state.cacheNames.findIndex(s => s === obj.name);       if (i < 0)         return;       state.cacheNames.splice(i, 1);     },   }, } 

2、路由信息配置,来自vue-router的路由配置,添加了几个自定义的属性。

  • 注意name和组件内部的name定义应该一致,会在<keep-alive :include="cacheNames">中使用。
  • meta.title:标题
  • meta.icon:icon图标
  • meta.affix:是否固定,固定在标签栏
{   path: '/home',     name: 'Home',     meta: { title: '首页', lang: 'home', icon: 'el-icon-s-home', affix: true },   	component: () => import('@/views/Home.vue'), }, {   path: '/books',   name: 'Books',   meta: { title: '图书管理', lang: 'book', icon: 'el-icon-notebook-2' },   component: () => import('@/views/book/Books.vue'), }, 

3、创建标签栏组件TabsBar.vue,核心功能、代码都在这里,标签的显示、功能操作,包括右键菜单。完整代码:Github / KWebNote

<template>   <div class="tabs-bar">     <router-link       class="item"       v-for="r in cachedRoutes"       :to="r"       :key="r.path"       :class="isActive(r)?'active':''"       @contextmenu.prevent.native="showMenu(r,$event)"       >       <i :class="r.meta.icon"></i>       {{r.meta?.lang ? $t('menu.' + r.meta.lang) : r.meta?.title}}       <i class="el-icon-close close" v-if="!isAffix(r)" @click.prevent.stop="handleClose(r)"></i>     </router-link>      <!-- 页签按钮的右键菜单 -->     <el-card class="menu" v-show="tagMenu.visible" :style="{left:tagMenu.left+'px',top:tagMenu.top+'px'}">       <ul>         <li @click="refresh(selectedTag)" v-show="isActive(selectedTag)">           <i class="el-icon-refresh"></i> 刷新         </li>         <li @click="handleClose()" v-show="!isAffix(selectedTag)">           <i class="el-icon-close"></i> 关闭         </li>         <li @click="handleCloseOther()">           <i class="el-icon-circle-close"></i> 关闭其他         </li>         <li @click="handleCloseAll">           <i class="el-icon-error"></i> 关闭所有         </li>       </ul>     </el-card>   </div> </template> 

❗注意:这里的标签关闭按钮,一定要加上修饰符“.prevent.stop”,阻止冒泡、及其他事件,因为标签本身也是有点击事件的,开始没加,莫名其妙没有跳转,被卡了好半天。

<i class="el-icon-close close" v-if="!isAffix(r)" @click.prevent.stop="handleClose(r)"></i> 

复习一下:

修饰符 描述
.stop 调用 event.stopPropagation()停止向上冒泡(propagation /ˌprɒpəˈɡeɪʃn/ 传播)
.prevent 调用 event.preventDefault()取消默认事件行为,如checkbox、<a>的默认事件行为,不影响冒泡

?刷新怎么实现?

刷新的实现稍微复杂一点点,因为这是本地路由,不能刷新整个页面,而当前路由视图是用了<keep-alive>缓存的。因此实现刷新的的基本过程:

  • 去除缓存并关闭路由:去除<keep-alive>的缓存,就是从其include白名单中移除。
  • 重新打开路由视图

为了视觉效果更佳,这里用一个中间页面进行跳转,效果如下:

图书商城项目练习①管理后台Vue2/ElementUI

设计了一个中级页面Redirect.vue,作用只有一个,就是用于跳转,跳转目标用路由参数传递。

// 用于中转跳转的页面 <script> export default {   created() {     this.$router.replace({ path: '/' + this.$route.params.path, query: this.$route.query });   },   render: function (h) {     return h()    } } </script> 

需要注意中间页面,不缓存、不显示标签栏。刷新时,移除缓存,然后重定向到当前页面,重新加载当前页面。

refresh(tag) {   //移除去掉缓存,再重定向跳转到当前页面   this.$store.commit('tabBars/removeName', this.$route);   this.$nextTick(() => {     this.$router.replace({       path: '/redirect' + tag.path     })   }) }, 

2.5、用户配置本地化保存

系统菜单“用户设置”,实现用户自定义的一些个性化配置,并本地存储、加载。保存到localStorage中,这样下次进入系统可以保持个性化配置了。

图书商城项目练习①管理后台Vue2/ElementUI

?效果如上图,需求分析

图书商城项目练习①管理后台Vue2/ElementUI

  • 主题样式,只实现了标题栏颜色样式,前景色color、背景色backgroundColor,应用在标题栏Header的样式上。
  • 路由切换动画是否开启。
  • 多语言设置,实现了中文、英文语言,详见下一章节。
  • 导航菜单的折叠状态。
  • 用户配置保存到本地localStorage中,系统初始化时从localStorage加载用户配置。

?‍♂️实现过程

1、创建一个单独的vue组件“UserConfig.vue”管理用户的设置项,内部用抽屉<el-drawer>来实现从右侧滑出的效果。
2、监听数据的变化(深度监听),如果变化则保存用户配置数据到localStorage,同时更新多语言的配置项。

created() {   //监听配置变更,持久化存储到本地   this.$watch('config', () => {     localStorage.setItem('admin-userconfig', JSON.stringify(this.config));     //手动更新语言     this.$i18n.locale = this.config.language;   }, { deep: true }) }, 

3、在main.js中添加初始化代码,从localStorage加载上次保存的用户配置信息。

created: function () {   LoadUserConfig(); } function LoadUserConfig() {   let vstr = localStorage.getItem('admin-userconfig');   if (vstr) {     Object.assign(userConfig, JSON.parse(vstr));     userConfig.thema = themas.filter(s => s.name == userConfig.thema.name)[0];     //语言     i18n.locale = userConfig.language;   }    

2.6、国际化多语言

实现多语言(国际化)的最主流、成熟的方案就是i18n(internationalization /ˌɪntəˌnæʃnəlaɪˈzeɪʃn/ 国际化,首字母i、尾字母n加中间的18个字母),官方文档,Vue中使用vue-i18n插件。

图书商城项目练习①管理后台Vue2/ElementUI

安装i18n插件,Vue2.*不太兼容最新版的v9.*,安装8.*版本:

vue add i18n # 或者 cnpm i -S vue-i18n@8.0.0 

安装完成后,“package.json”文件中就有了"vue-i18n": "^8.26.3"

vue add i18n方式安装,除了安装插件,还把基本的配置、语言文件都准备好了,属于完成了简装可以拧包入住了。npm指令安装只会安装插件,需要自己完成配置和注册。

?配置i18n

i18n的配置、使用还是比较简单的,先配置语言信息,然后在代码(JavaScript、Vue模板)中使用。

|- src   |-lang     |-index.js   # 配置i18n     |-lang-en.js # 英文语言资源     |-lang-cn.js # 中文语言资源 

1、分别创建不同的语言包文件,语言信息为一个键值结构的JSON对象,键为语言项的key,值为显示的文本内容。结构可以按照项目情况自行定义,叶子节点属性是一个语言项。

图书商城项目练习①管理后台Vue2/ElementUI

2、注册插件,配置i18n实例。

import Vue from 'vue' import VueI18n from 'vue-i18n' import lang_zhcn from './lang-cn' import lang_en from './lang-en' //注册 Vue.use(VueI18n); //申明i18n const i18n = new VueI18n({   locale: 'en',     //选中的语言   messages: {     en: {       ...lang_en,   //英文语言配置     },     zh: {       ...lang_zhcn, //中文语言配置     }   } }) 
  • locale属性为当前选中的语言,更改值实现语言切换。
  • Element的国际化,按照官方文档配置即可:element-国际化

3、在main.js中引入,并注入到Vue根实例中。

import i18n from './lang' new Vue({   router,   store,   i18n, }).$mount('#app') 

?使用

使用就简单了,使用i18n提供的方法$t('key')即可。

<p class="title">{{$t('home.user')}}</p> //JS title(item) {   return item.meta?.lang ? this.$t('menu.' + item.meta.lang) : item.meta?.title; } 

03、登录页面/Login.vue

图书商城项目练习①管理后台Vue2/ElementUI

登录页面主要就是用户名、密码的表单,然后调用后端登录api接口验证、获得token完成登录。

{   user: { name: '', pwd: '' },   rules: {     name: [{ required: true, message: '用户名不能为空' }, { min: 3, max: 8, message: "长度应为3-8" }],     pwd: [{ required: true, message: '密码不能为空' }, { min: 3, max: 8, message: "长度应为3-8" }],   }, } 
  • 表单验证:表单验证的执行是在表单<el-form>组件上调用validate()方法,因此需要绑定model对象,在表单项<el-form-item>``prop上绑定model对象字段名。
this.$refs.userForm.validate((valid, mes) => {   if (!valid) {     this.$message.error('输入有误,请修改后重新提交!');     return;   }   //提交... 
  • 记住用户名:保存到本地localStorage中,加载该页面的时候读取。
  • 登录成功:获取并保存token信息,保存在vuexstore中或本地sessionStorage,然后跳转到主页面 this.$router.push('/home')

04、首页/Home.vue

管理类系统大概率都有一个首页Home.vue,作为默认页面,展示系统的一些概况、用户的一些统计信息、通知信息等。为保持各个“豆腐块”风格一致,推荐用<el-card>组件包装内容。

图书商城项目练习①管理后台Vue2/ElementUI

用到了图表组件 echarts,安装最新版本:

$ cnpm install echarts -S  // 引入echarts import * as echarts from 'echarts' // 在Vue原型上挂载$echarts,在vue示例中this.$echarts Vue.prototype.$echarts = echarts 

这里只用了2个图表,却引入了所有的echarts组件,打包后的JS文件6M多,实在太大了。通过官方提供的在线定制功能按需定制JS文件,然后引入该JS文件即可。


05、图书管理模块

图书管理为图书的综合管理,包含,是管理后台的典型功能,如用户管理、商品管理、活动管理、公告管理等等都类似。

图书商城项目练习①管理后台Vue2/ElementUI

功能结构如下图:

图书商城项目练习①管理后台Vue2/ElementUI

代码结构如下图,图书管理模块包含多个页面,其中“Books.vue”为入口页面。

图书商城项目练习①管理后台Vue2/ElementUI

5.1、图书列表

图书商城项目练习①管理后台Vue2/ElementUI

  • 图书列表由表格<el-table>和分页<el-pagination>组成。统一设计了查询结构,分页组件做了一个简单的封装。
  • 表格中的状态用到了一个自己写的枚举组件,详见《前端枚举enum的应用(Element)封装》
  • 图书详情用了抽屉组件<el-drawer>,点击名称从右侧弹出。
  • 图书的新增、修改用的弹框组件<el-dialog>,默认的BookDialog是弹出带遮罩的模态框,额外实现了一个Plus版本BookDialogPlus(详见下文)。

5.2、分页组件

分页是列表常用组件,Element-UI提供了分页组件<el-pagination>,在此基础上做一个简单的封装,统一规范、简化使用。

图书商城项目练习①管理后台Vue2/ElementUI

✔️封装了些什么?

  • 统一风格样式、布局,也方便统一修改。
  • 统一分页大小配置page-sizes="[5, 10, 20, 50]"
  • 统一分页事件pagination,统一处理了页码、页数的变更。
<template>   <el-pagination     style="text-align:right;margin:6px 2px" background     :total="total" :current-page="currentPage" :page-size="pageSize" :page-sizes="[5, 10, 20, 50]"     @current-change="pageChanged" @size-change="pageSizeChanged"     layout="total, sizes, prev, pager, next, jumper"     ></el-pagination> </template> <script>   export default {     props: {       //总数       total: { type: Number, default: 0, },       //页码,外部绑定,加修饰符.sync       size: { type: Number, default: 10, },       // 当前页码,外部绑定,加修饰符.sync       index: { type: Number, default: 1, }     },     computed: {       // 用修饰符“.sync”来实现更新父组件的值       currentPage: {         get() { return this.index },         set(val) { this.$emit('update:index', val) }       },       pageSize: {         get() { return this.size },         set(val) { this.$emit('update:size', val) }       }     },     methods: {       pageSizeChanged(v) {         // 修改父组件值         this.$emit('update:size', v);         // 触发分页事件         this.$emit('pagination');       },       pageChanged(v) {         // 修改父组件值         this.$emit('update:index', v);         // 触发分页事件         this.$emit('pagination');       },     }   } </script> 

这里的页行数size、页码index,是外部传入的的prop值,但内部也会修改。导致了“双重绑定”更新,这就需要用到.sync修饰符了,其实就是基于事件通知实现的,可参考官网文档

  • 子组件内部通过Vue的事件this.$emit('update:myPropName', v);触发变更通知。
  • 外部绑定的时候用.sync修饰符,.sync实现了更新update事件的监听和赋值,需注意不支持表达式,只能用property名。

?使用

<Pagination :total="total" :size.sync="search.size" :index.sync="search.index" @pagination="loadData"></Pagination> 

5.3、图书编辑Plus

如下效果图,相比常规的模态框,只是遮住了当前视图(图书管理),不影响其他功能操作。

图书商城项目练习①管理后台Vue2/ElementUI

用的依然是弹框组件<el-dialog>,在此基础上做了一点点调整。完整代码见 Github / KWebNote

图书商城项目练习①管理后台Vue2/ElementUI

  • 取消遮罩层:modal="false"
  • 操作按钮放到了标题栏上。
  • 通过样式让弹层刚好覆盖到当前视图。
.dialogPlus {   position: absolute;   overflow: inherit;   .el-dialog {     min-height: 100% !important;     max-height: 100%;     display: flex;     flex-flow: column;     .el-dialog__header {       padding: 4px 10px;     }     .el-dialog__body {       overflow: auto;       max-height: 100%;     }   } } 

5.4、图片上传upLoad

图片上传使用文件上传组件<el-upload>,这里涉及一些基础通用操作,因此针对图片上传封装为一个组件“ImgUpload.vue”,效果如下:

图书商城项目练习①管理后台Vue2/ElementUI

  • 上传接口action,后端文件接口实现详见后端章节。
  • 文件格式accept="image/*",这里的accept值为文件的类型
  • 文件数量limit,配置最大支持的文件个数,钩子on-exceed超过文件数量限制时触发。当达到限制时,隐藏上传按钮。
  • 上传前before-upload上传前的钩子,可用来验证文件的合法性。
  • 上传成功on-success文件上传成功的钩子,可用来同步上传的文件资源。
  • 文件信息prop."value",组件中定义了一个value的props,接受父组件传入的已有文件集合(字符串,多个逗号隔开),组件内文件变化通过this.$emit('input',nval)更新value值。
  • 预览:用一个<el-dialog>来展示预览图。
<template>   <div>     <el-upload       ref="upload" :action="$api.URL.upload" list-type="picture-card"       :multiple="true" accept="image/*" name="file"       :limit="limit" :class="{hide:uploadHide}" :file-list="fileList"       :on-exceed="onOutOfLimit" :on-success="handleSuccess"       :on-error="handleError" :before-upload="handeleBefore" >       <!-- 上传按钮 -->       <i slot="default" class="el-icon-plus"></i>       <!-- 提示内容 -->       <div slot="tip" style="font-size:0.8em">支持最多{{limit}}张图片,每张图片不超过{{maxSize}}Kb</div>       <!-- file模板 -->       <div slot="file" slot-scope="{file}" class="imgbox" :class="{success:file.status}">         <!-- 缩略图的路径,如果相对路径则添加代理前缀-->         <img :src="proxyURL(file.url)" alt />         <span class="el-upload-list__item-actions">           <span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">             <i class="el-icon-zoom-in"></i>           </span>           <span class="el-upload-list__item-delete" @click="handleRemove(file)">             <i class="el-icon-delete"></i>           </span>         </span>       </div>     </el-upload>     <!-- 嵌套的dialog,需要设置append-to-body,嵌入自身到body元素 -->     <el-dialog :visible.sync="dialogVisible" append-to-body custom-class="imgdialog">       <img :src="proxyURL(dialogImageUrl)" alt style="max-width: 100%;max-heigt: 100%;object-fit:contain" />     </el-dialog>   </div> </template> 

再加上一点点JS和CSS就完成了,完整代码见 Github / KWebNote。。使用:

import ImgUpload from '@/components/ImgUpload.vue'  <ImgUpload v-model="book.imgs"></ImgUpload> 

5.5、富文本@wangeditor

wangEditor 是一个轻量级 web 富文本编辑器,配置方便,使用简单。安装vue版本的“@wangeditor/editor-for-vue”:$ cnpm i @wangeditor/editor-for-vue -S。然后使用参考官方vue使用文档,Ctrl+CV即可。
要实现图片、视频上传还需要自己配置,因此就基于@wangeditor封装了一个富文本编辑器Editor.vue,效果如下。

  • 调整了工具栏,排除了一些不需要的。
  • 配置了图片上传服务,@wangeditor支持粘贴图片。

图书商城项目练习①管理后台Vue2/ElementUI

?配置工具栏

默认提供的工具栏功能比较丰富,如果需要调整,则需要先获取工具栏的toolbarKeys。引入@wangeditor/editor,在编辑器组件准备完成后获取toolbarKeys,如updated()

import { DomEditor } from '@wangeditor/editor'  updated() {   ////在这里获取工具栏的配置toolbarKeys,用于自定义配置工具栏       const toolbar = DomEditor.getToolbar(this.editor)   console.log(toolbar.getConfig().toolbarKeys) }, 

获取到的keys如下(整理后):

[   "headerSelect", "blockquote",   "|",   "bold", "underline", "italic",   {     "key": "group-more-style", "title": "更多", "iconSvg": "",     "menuKeys": [ "through", "code", "sup", "sub", "clearStyle" ]   },   "color", "bgColor",   "|",   "fontSize", "fontFamily", "lineHeight",   "|",   "bulletedList", "numberedList", "todo",   {     "key": "group-justify", "title": "对齐", "iconSvg": "",     "menuKeys": [ "justifyLeft", "justifyRight", "justifyCenter", "justifyJustify" ]   },   {     "key": "group-indent", "title": "缩进", "iconSvg": "",     "menuKeys": [ "indent", "delIndent" ]   },   "|",    "emotion", "insertLink",   {     "key": "group-image", "title": "图片", "iconSvg": "",     "menuKeys": [ "insertImage", "uploadImage" ]   },   {     "key": "group-video",     "title": "视频", "iconSvg": "",     "menuKeys": [ "insertVideo", "uploadVideo" ]   },   "insertTable", "codeBlock", "divider",   "|",   "undo", "redo", "|", "fullScreen" ] 

通过toolbarConfig.excludeKeys配置不需要的工具栏按钮:

data() {   return {     editor: null,     toolbarConfig: { excludeKeys: ['group-video', 'emotion', 'lineHeight'] },     editorConfig: { placeholder: '请输入内容...', maxLength: 8000 },     mode: 'default', // default simple   } }, 

完整代码见 Github / KWebNote

?配置图片上传

在Editor的配置项editorConfig中配置图片上传参数,如下代码:

editorConfig: {   placeholder: '请输入内容...', maxLength: 8000,     MENU_CONF: {      uploadImage: { //配置图片上传       server: this.$api.URL.upload,  //后端文件上传地址         fieldName: 'file', //表单参数名,和后端一致         maxFileSize: 2 * 2048 * 2048,  //最大文件大小         maxNumberOfFiles: 1, //每次文件个数         allowedFileTypes: ['image/*'], //文件类型:图片         timeout: 9 * 1000,  //超时时长         // 自定义插入图片,根据后端返回的结构,加上跨域代理         customInsert: (res, insertFn) => {         const url = this.$api.URL.proxy + res.url         insertFn(url)       },     }   } }, 

5.6、树形下拉框

图书的类型是来自字典数据(详见后续《字典管理》章节),树形结构,ElementUI2版本中么有树形的下拉框组件,Element3(ElementPlus)有。结合下拉框组件<el-select>和树形组件<el-tree>封装实现了一个树形下拉框组件TreeSelect,效果图如下。

图书商城项目练习①管理后台Vue2/ElementUI

<el-tree>作为<el-select>的一个选项值<el-option>,然后JS代码实现选择值的同步管理即可,逻辑比较简单。

<el-select v-model="currentText" placeholder="请选择" @clear="handelClear" clearable>   <el-option class="option view-scroll" :value="currentItem[options.value]" :label="currentItem[options.label]">     <!-- data:数据-->     <!-- props:数据结构配置 -->     <!-- node-key:唯一标识字段 -->     <el-tree ref="tree" :data="data" :node-key="options.value" :props="options" class="tree" @current-change="handleCurrentChange"></el-tree>   </el-option> </el-select> 

完整代码见Github / KWebNote


06、字典管理模块

字典管理为一个比较通用的字典数据管理模块Dictionary.vue,用来管理一些可变的分类数据,如图书分类、商品促销类型、品牌、国家、省市区地址等。包含两部分数据:

  • 字典类别,定义有哪些字典类别,包含分类名称、编码、是否树形结构等关键字段。
  • 字典数据,每一个字典类别的字典数据,统一存储,用字典编码区分,树形结构。结构:id、名称、类别编码、排序号、父id。

图书商城项目练习①管理后台Vue2/ElementUI

树形结构的数据编辑时,可以选择父级,这里用的是<el-cascader>级联选择器组件。

图书商城项目练习①管理后台Vue2/ElementUI

数据是在本地进行树形组装和排序的,根级节点的父idpid为0,用buildDicTree方法递归构造一颗树。

export function queryDicData(type, istree = false) {   return api.dicdata({ code: type }).then(res => {     if (!res.data || res.data.length <= 0) return [];     //构造树形结构     if (!istree)       return res.data.sort(sortDicData);     let sortItems = buildDicTree(res.data, ROOT_PID);     return sortItems;   }) } function buildDicTree(items, pid) {   let sortItems = items.filter(s => s.pid == pid);   if (!sortItems || sortItems.length <= 0) return [];   sortItems.sort(sortDicData).forEach(item => {     const res = buildDicTree(items, item.id);     if (res && res.length > 0)       item.children = res.sort(sortDicData);   });   return sortItems; } function sortDicData(item1, item2) {   return item1.sort - item2.sort; } 

?需要注意的是,这里修改字典数据时,父级节点不能选择自己及自己的子节点,否则会导致死循环。因此需要对上面构造的树做一个处理,把不能选择的节点设置disabled属性。


参考资料


©️版权申明:版权所有@安木夕,本文内容仅供学习,欢迎指正、交流,转载请注明出处!原文编辑地址-语雀