- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
本系列文章是为学习Vue的项目练习笔记,尽量详细记录一下一个完整项目的开发过程。面向初学者,本人也是初学者,搬砖技术还不成熟。项目在技术上前端为主,包含一些后端代码,从基础的数据库(Sqlite)、到后端服务Node.js(Express),再到Web端的Vue,包含服务端、管理后台、商城网站、小程序/App,分为下面多个篇文档.
🪧系列目录 :
- 图书商城Vue+Element+Node+TS项目练习 🔗
- 图书商城①管理后台Vue2+ElementUI 🔗
- 图书商城②后端服务Node+Express+Sqlite 🔗
- 未完成:商城网站Vue3+TS、商城APP端Vue3+TS+uniapp
这是一个比较典型的管理后台练习项目,包含登录、框架页、导航路由、导航标签、数据管理、字典管理等基础功能。管理后台的业务大多是数据管理CRUD功能,该项目只是是简单实现了几个模块。同时针对CRUD,整理了一个模板📁template.
🔸 技术路线 :
🔸 相关组件 :
vuex
:状态管理 vue-router
:前端路由 axios
:HTTP调用 echarts
:图表组件,按需定制 i18n
:多语言国际化 vue-i18n
v8.*版本 @wangeditor
:富文本编辑器 Less
:CSS预处理器/语言 🔸 源代码地址 : Github / KWebNote , Gitee / KWebNote ,管理后台代码在目录📁 book_admin 下.
🔸 在线体验地址 🔥🔥 : http://kanding.gitee.io/kwebnote (任意用户名、密码。通过gitee静态页面Gitee Pages部署的,所以这里部署的版本是写了个mock模拟api,路由用的hash模式).
创建图书管理后台项目“ book_admin ”,基于 @vue/cli ,通过其 vue ui 管理工具,可视化操作创建项目.
@vue/cli
参考《 Vue项目工程@vue/cli入门 》 vue创建的项目已经包含了一个基础的架子了,如下图,主html页面文件“ index.html ”,入口JS文件“ main.js ”,入口Vue文件“ App.vue ”.
管理后端是SPA单页应用,创建主框架页面“Main.vue”,登录后的所有内容都在这个主页面内呈现和管理。页面视图关系如下图:
因此,主页面就比较重要,是搬砖的基座,实际效果和布局结构图如下:
<keep-alive>
,配合多标签组件视图缓存,切换标签后视图的状态会被保持。 <transition>
,切换内容时的动画效果。
<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>
每个后台系统都会有导航菜单,支持多级展示、可收缩,效果如下图:
👷♂️实现过程 :
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>
就是在路由切换页面视图的时候,有一个过渡动画效果,如下图.
主要是使用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 用了一个用户配置属性,目的是可以配置是否开启路由转场动画.
如下效果图,类似Chrome浏览器的多标签效果,打开的路由视图标题显示在标签栏,可以刷新、关闭、切换。切换时会保留视图状态,这样就可以很方便的多标签操作了.
<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
白名单中移除。 为了视觉效果更佳,这里用一个中间页面进行跳转,效果如下:
设计了一个中级页面 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
})
})
},
系统菜单“用户设置”,实现用户自定义的一些个性化配置,并本地存储、加载。保存到 localStorage 中,这样下次进入系统可以保持个性化配置了.
🔵效果如上图,需求分析 :
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;
}
实现多语言(国际化)的最主流、成熟的方案就是 i18n (internationalization /ˌɪntəˌnæʃnəlaɪˈzeɪʃn/ 国际化,首字母 i 、尾字母 n 加中间的18个字母), 官方文档 ,Vue中使用 vue-i18n 插件.
安装 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 的配置、使用还是比较简单的,先配置语言信息,然后在代码(JavaScript、Vue模板)中使用.
|- src
|-lang
|-index.js # 配置i18n
|-lang-en.js # 英文语言资源
|-lang-cn.js # 中文语言资源
1、分别创建不同的语言包文件,语言信息为一个键值结构的JSON对象,键为语言项的key,值为显示的文本内容。结构可以按照项目情况自行定义,叶子节点属性是一个语言项.
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
属性为当前选中的语言,更改值实现语言切换。 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;
}
登录页面主要就是用户名、密码的表单,然后调用后端登录 api 接口验证、获得 token 完成登录.
rules
, element表单验证 。
{
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
信息,保存在 vuex
的 store
中或本地 sessionStorage
,然后跳转到主页面 this.$router.push('/home')
。 管理类系统大概率都有一个首页 Home.vue ,作为默认页面,展示系统的一些概况、用户的一些统计信息、通知信息等。为保持各个“豆腐块”风格一致,推荐用 <el-card> 组件包装内容.
用到了图表组件 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文件即可.
图书管理为图书的综合管理,包含 增 、 删 、 改 、 查 ,是管理后台的典型功能,如用户管理、商品管理、活动管理、公告管理等等都类似.
功能结构如下图:
代码结构如下图,图书管理模块包含多个页面,其中“ Books.vue ”为入口页面.
<el-table>
和分页 <el-pagination>
组成。统一设计了查询结构,分页组件做了一个简单的封装。 <el-drawer>
,点击名称从右侧弹出。 <el-dialog>
,默认的 BookDialog
是弹出带遮罩的模态框,额外实现了一个Plus版本 BookDialogPlus
(详见下文)。 分页是列表常用组件,Element-UI提供了分页组件 <el-pagination> ,在此基础上做一个简单的封装,统一规范、简化使用.
✔️封装了些什么?
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 修饰符了,其实就是基于事件通知实现的,可参考 官网文档 .
this.$emit('update:myPropName', v);
触发变更通知。 .sync
修饰符, .sync
实现了更新update事件的监听和赋值,需注意不支持表达式,只能用property名。 🟢使用 :
<Pagination :total="total" :size.sync="search.size" :index.sync="search.index" @pagination="loadData"></Pagination>
如下效果图,相比常规的模态框,只是遮住了当前视图(图书管理),不影响其他功能操作.
用的依然是弹框组件 <el-dialog> ,在此基础上做了一点点调整。完整代码见 Github / KWebNote .
: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%;
}
}
}
图片上传使用文件上传组件 <el-upload> ,这里涉及一些基础通用操作,因此针对图片上传封装为一个组件“ ImgUpload.vue ”,效果如下:
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>
wangEditor 是一个轻量级 web 富文本编辑器,配置方便,使用简单。安装vue版本的“@wangeditor/editor-for-vue”: $ cnpm i @wangeditor/editor-for-vue -S 。然后使用参考官方 vue使用文档 ,Ctrl+CV即可。 要实现图片、视频上传还需要自己配置,因此就基于 @wangeditor 封装了一个富文本编辑器 Editor.vue ,效果如下.
@wangeditor
支持粘贴图片。
默认提供的工具栏功能比较丰富,如果需要调整,则需要先获取工具栏的 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)
},
}
}
},
图书的类型是来自字典数据(详见后续《字典管理》章节),树形结构,ElementUI2版本中么有树形的下拉框组件,Element3(ElementPlus)有。结合下拉框组件 <el-select> 和树形组件 <el-tree> 封装实现了一个树形下拉框组件 TreeSelect ,效果图如下.
<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 .
字典管理为一个比较通用的字典数据管理模块 Dictionary.vue ,用来管理一些可变的分类数据,如图书分类、商品促销类型、品牌、国家、省市区地址等。包含两部分数据:
树形结构的数据编辑时,可以选择父级,这里用的是 <el-cascader> 级联选择器组件.
数据是在本地进行树形组装和排序的,根级节点的父id pid 为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 属性.
©️版权申明 :版权所有@安木夕,本文内容仅供学习,欢迎指正、交流,转载请注明出处! 原文编辑地址-语雀 。
最后此篇关于图书商城项目练习①管理后台Vue2/ElementUI的文章就讲到这里了,如果你想了解更多关于图书商城项目练习①管理后台Vue2/ElementUI的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我会尽可能地解释我正在做的事情,以获得最好的可能的建议/解决方案。这一切都是在 java 中完成的。 我的客户有一个基于 SWING 的桌面应用程序,它将使用 WebStart 加载。我被指派为用户帐
看来这个page包含 Azure CLI 支持的与 Azure API 管理相关的所有功能。但它没有展示如何使用 Azure CLI 管理用户、产品、证书、订阅和 API 等实体。 Azure CLI
我设置了一个 Hadoop 1.2.x 版本,双节点集群。第一节点(NameNode、Jobtracker)和第二节点(Secondary NameNode、Datanode、TaskTracker)
对于内容驱动的网站,设计好坏的关键是关系型数据库。在这个教程中,我们已经使用了MySQL关系型数据库管理系统(RDBMS)建立了我们的数据库。对于网站的开发者来说,MySQL是一个较受欢迎的选择,这
在尝试运行MariaDB之前,首先确定其当前状态,运行或关闭。 有三个选项用于启动和停止MariaDB – 运行mysqld(MariaDB脚本)。 运行mysqld_safe启动脚本。
我在管理界面中遇到 StackedInlines 前缀的问题。我会尝试发布所有必要的代码。 models.py(简要) ##### Base classes class BaseItem(models
我是新来的。到目前为止,我一直在使用 MVC 模型并使用基本的 session 管理模型,即在 session 中存储一个 token 并检查每个请求。 我正在尝试对lift做同样的事情,但我的 se
我在 win 服务中使用 NHiberante。有时我得到 System.ObjectDisposedException: Session is closed! Object name: 'ISess
我正在尝试使用 HtmlUnit 登录 Facebook 页面并查看其 HTML 内容。我正在尝试通过 HtmlUnit 填写登录凭据,但在单击提交按钮时我没有看到正在执行的 session 。 在
我正在为一个相当大的项目开发一个带有 reactjs 的前端,该项目有两个主要接口(interface)。主站点的前端和管理员的前端。 我应该将它们开发为两个不同的项目还是 reactjs 中的一个项
短版 我有一个使用插件基础结构的应用程序。插件具有可配置的属性,可帮助它们了解如何完成工作。插件按配置文件分组以定义如何完成任务,配置文件存储在由 DataContractSerializer 序列化
如何管理 iPhone 应用程序中的用户 session ?我在应用程序的第一页上从用户那里获取了用户名和密码。用户可以随时注销。如何像其他 Web 应用程序一样在 iPhone 应用程序中存储 se
我正在使用 Azure API 管理,其中包含第三方论坛 (Discourse) 的链接。 api管理提供的默认登录系统用于注册用户。我想知道是否可以对 api 管理和论坛使用单点登录,这样用户就不必
我正在使用 Wordpress 建立一个网站,并且我想利用它的 session 。但我没有找到任何插件,甚至文档。在我开始破解之前有什么建议或引用吗? 注意:我问的是 WP 是否以及如何使用标准 PH
我已阅读《Azure in Action》一书中的以下内容:“在 Windows Azure 中,状态服务器或进程外 session 状态提供程序,不支持” 谁能告诉我为什么不支持这个。他们在书中没有
我有一个内联表单集,我想排除一些模型对象在表单集中显示。 例如。模型 B 具有模型 A 的外键,因此它是 1:n(A 对象有许多 B 对象)关系。现在在 A 管理编辑页面上,我已经获得了 B 的内联。
我正在开发一个基于 session 的项目。我在想,与银行类似,我会创建一张支票并为用户提供阻止 session 超时的能力。 我正在考虑创建一个 setInterval 来检查需要身份验证的空白页面
我正在为一位拥有 Magento 商店的客户工作。里面塞满了产品,但这些产品的名称有点乱。他并没有坚持一种命名约定,而是多年来使用了不同的约定。因此,每当他使用“管理”->“管理产品”部分中的“名称”
我使用大约十几个 XSLT 文件来提供大量输出格式。目前,用户必须知道导出的文件格式的扩展名,例如RTF、HTML、TXT。 我还想使用参数来允许更多选项。如果我可以将元数据嵌入 XSL 文件本身,那
我已阅读《Azure in Action》一书中的以下内容:“在 Windows Azure 中,状态服务器或进程外 session 状态提供程序,不支持” 谁能告诉我为什么不支持这个。他们在书中没有
我是一名优秀的程序员,十分优秀!