之前一直忙于公司业务扩展后的填需求,现在终于有机会好好总结下在项目中一些优秀的实践,希望也会对你的开发有所启发。
Layout组件
对于一个控制台项目,他总有些登录后就不会再修改的部分,比如侧边菜单栏、顶部底部导航栏,在Vue中,我们可以通过嵌套路由来实现。这样做,在页面切换时,用户体验会更加平滑。
目录结构:
├── src
├── components
└── common
├── Sidebar # 侧边菜单栏
│ ├── MenuItem.vue # 菜单子项
│ └── index.vue # 菜单栏
├── Header.vue # 顶部导航
└── Layout.vue # Layout组件
Layout.vue:
<template>
<div class="wrapper">
<v-sidebar></v-sidebar>
<div class="content-box">
<v-head></v-head>
<div class="content">
<transition name="move" mode="out-in">
<router-view></router-view>
</transition>
</div>
</div>
</div>
</template>
<script>
import vHead from './Header.vue'
import vSidebar from './Sidebar/Index.vue'
export default {
name: 'Layout',
components: {
vHead,
vSidebar
}
}
</script>
router/index.js:
export const constRoutes = [{
path: '/',
name: 'home',
component: Layout,
redirect: '/home/index',
children: [{
path: '/404',
component: () =>
import(/* webpackChunkName: "404" */ '@page/error/404.vue'),
meta: { title: '404' }
},
{
path: '/403',
component: () =>
import(/* webpackChunkName: "403" */ '@page/error/403.vue'),
meta: { title: '403' }
}]
}]
权限控制
权限控制是每个控制台都逃不掉的课题,最普遍简单的做法就是通过constRoutes
静态路由和asyncRoutes
动态路由来实现。
这里我们做个小小的升级,为了可以更灵活的配置权限,除了可配置的角色权限外,我们还额外引入一个全局可以展示的所有菜单列表,添加这个列表的好处是,当我们在版本迭代时,会存在删减需求的情况,这时候比起一个个角色修改可显示菜单,还是直接修改可展示列表更为高效便捷。
权限控制的流程:
- 未登录的情况跳转登录页面
- 用户登录获取token及权限可访问菜单
- 在浏览器地址栏输入访问地址时比较可展示菜单和用户可访问菜单,满足条件则跳转
- 菜单栏比较可展示菜单和用户可访问菜单显示符合条件的菜单
目录结构:
├── router
│ ├── modules # 划分路由
│ │ ├── page.js # page菜单下所有路由配置
│ │ └── setting.js # setting菜单下所有路由配置
│ └── index.js # 路由主路径
├── utils
└── menulist.js # 所有可展示菜单
让我们直接来看看router/index.js文件:
import Vue from 'vue'
import Router from 'vue-router'
import { allPermissions } from '@/utils/menulist.js'
import Layout from '@/components/common/Layout'
import store from '../store'
Vue.use(Router)
export const constRoutes = [{
path: '/login',
name: 'login',
component: () => import(/* webpackChunkName: "login" */ '@page/Login.vue')
}, {
path: '/',
name: 'home',
component: Layout,
redirect: '/overview/index',
children: [{
path: '/404',
component: () =>
import(/* webpackChunkName: "404" */ '@page/error/404.vue'),
meta: { title: '404' }
},
{
path: '/403',
component: () =>
import(/* webpackChunkName: "403" */ '@page/error/403.vue'),
meta: { title: '403' }
}]
}]
const routes = []
const files = require.context('./modules', false, /\w+.js$/)
files.keys().forEach(fileName => {
// 获取模块
const file = files(fileName)
routes.push(file.default || file)
})
export const asyncRoutes = [
...routes,
{
path: '/log',
name: 'log',
meta: { title: '日志', icon: 'el-icon-s-management', roles: ['admin'] },
component: Layout,
children: [{
path: 'index',
name: 'log_index',
meta: { title: '操作记录', icon: 'el-icon-s-custom', roles: ['admin'] },
component: () => import(/* webpackChunkName: "log_index" */ '@page/log/index')
}]
},
{
path: '*',
redirect: '/404',
hidden: true
}
]
const router = new Router({
routes: constRoutes.concat(asyncRoutes)
})
router.beforeEach((to, from, next) => {
const hasToken = store.state.user.userId
const permissions = store.getters.permissions
if (hasToken) {
if (to.path === '/login') {
next({ path: '/' })
} else if (allPermissions.includes(to.name) && !permissions.includes(to.name)) {
next({ path: '/404' })
} else {
next()
}
} else {
if (to.path !== '/login') {
next('/login')
} else {
next()
}
}
})
export default router
这里我们会发现,所谓的asyncRoutes
其实并不是从后台返回的,它包含了所有我们定义的路由,真正的控制其实是在用户信息的permissions
中实现的,为什么是这么做的呢,因为大多数时候后台保存的权限表并不是完整的路由信息,他可能只包含了路由的name或是path,为了达到真实的控制,我们只需要将asyncRoutes
和他比较就可以了。
分离对全局Vue的拓展
在项目中,我们经常会在全局Vue上做很多拓展,为了项目将来可以更方便的迁移拓展,我们可以做个小小的优化,将项目特有的拓展抽离成一个文件,也方便后期的维护。
目录结构:
├── main.js
├── app.js
main.js:
import Vue from './app.js'
import router from './router'
import store from './store'
import App from './App.Vue'
new Vue({
store,
router,
render: h => h(App)
}).$mount('#app')
这个main.js里就是最纯粹原始的Vue实例创建创建,当我们需要迁移时,只需要修改Vue的来源。
app.js
import Vue from 'vue'
import http from '@/utils/http'
import ElementUI from 'element-ui'
import contentmenu from 'v-contextmenu'
import 'v-contextmenu/dist/index.css'
import 'element-ui/lib/theme-chalk/index.css' // 默认主题
import './assets/css/icon.css'
Vue.config.productionTip = false
Vue.prototype.$http = http
Vue.use(contentmenu)
Vue.use(ElementUI, {
size: 'small'
})
export default Vue
这里举例的app.js就拓展引入了第三方的库。
axios的封装
通常项目中,为了做一些请求状态的拦截,我们会对axios再做一层封装,这其中也可以引入例如elemenet的加载组件,给所有的请求做一个过渡状态。
这里示例的例子主要在axios上拓展了三件事:
- 对所有的post请求添加loading动画
- 针对身份信息错误的情况,清空身份信息,跳转登录界面
- 针对请求返回错误状态的提示
目录结构:
├── src
├── utils
└── http.js # 封装axios
http.js:
import axios from 'axios'
import { MessageBox, Message, Loading } from 'element-ui'
import router from '@/router'
import store from '@/store'
const http = axios.create({
baseURL: '/console',
timeout: 10000
})
let loading = null
let waiting = false
http.interceptors.request.use(
config => {
if (config.method !== 'get') {
loading = Loading.service({ fullscreen: true })
}
return config
},
error => {
return Promise.reject(error)
}
)
http.interceptors.response.use(
response => {
loading && loading.close()
return response.data
},
error => {
loading && loading.close()
console.log('error', error.message)
if (error.message && error.message.indexOf('timeout') > -1) {
Message({
message: '请求超时',
type: 'error',
duration: 3 * 1000
})
return Promise.reject(error)
}
// 对错误状态码进行处理
const { status, data: { message } } = error.response
if (status === 401) {
if (!waiting) {
waiting = true
// 登录状态不正确
MessageBox.alert('登录状态异常,请重新登录', '确认登录信息', {
confirmButtonText: '重新登录',
type: 'warning',
callback: () => {
waiting = false
store.commit('clearUserInfo')
router.replace({ name: 'login' })
}
})
}
return Promise.reject(error)
}
if (status === 404) {
return Promise.reject(error)
}
Message({
message,
type: 'error',
duration: 3 * 1000
})
return Promise.reject(error)
}
)
export default http
app.js:
import Vue from 'vue'
import http from '@/utils/http'
Vue.prototype.$http = http
这里直接把封装好的请求挂在了Vue上,可以方便之后再组件中使用。
组件中使用:
<template>
<div>{{price}}</div>
</template>
<script>
export default {
data () {
return {
price: 0
}
}
method: {
getData () {
this.$http.get(`/getPrice`).then((data) =>
this.price = data
})
}
}
}
</script>
全局组件注册
项目中必然会存在一些全局公用的组件,但如果我们一个个去注册会很麻烦,所以这里我们把全局组件提到一个专门的目录下,通过一个registerComponent
的方法,批量注册,以后,我们就可以直接在页面里引用这些组件。
目录结构:
├── src
├── components
└── global # 存放全局组件的目录
├── TableData.vue # 全局组件
└── index.js # 用来批量处理组件组册的函数入口
index.js:
export default function registerComponent (Vue) {
const modules = require.context('./', false, /\w+\.Vue$/)
modules.keys().forEach(fileName => {
const component = modules(fileName)
const name = fileName.replace(/^\.\/(.*)\.\w+$/, '$1')
Vue.component(name, component.default)
})
}
app.js:
import Vue from 'vue'
import registerComponent from './components/global'
registerComponent(Vue)
页面中使用:
<template>
<div>
<TableData></TableData>
</div>
</template>
页面中无需引入组件,可以直接使用。
全局过滤器注册
在项目中,我们会频繁遇到对诸如时间、金额的格式化,将他们作为全局的过滤器,将更方便我们后续的使用。
目录结构:
├── src
├── utils
└── filters.js # 存放全局过滤器函数入口
filters.js:
export const formatPrice = (value, fixed = 2) => {
if (!value) {
return Number(0).toFixed(fixed)
}
return Number(value / 10 ** fixed).toFixed(fixed)
}
export const formatDate = (date, split = '-') => {
if (!date) return ''
const _date = new Date(date)
let year = _date.getFullYear()
let month = _date.getMonth() + 1
let day = _date.getDate()
return [year, month.toString().padStart(2, '0'), day.toString().padStart(2, '0')].join(split)
}
export const formatTime = (time) => {
if (!time) return ''
const _date = new Date(time)
let year = _date.getFullYear()
let month = _date.getMonth() + 1
let day = _date.getDate()
let hour = _date.getHours()
let minute = _date.getMinutes()
return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')} ${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`
}
export const formatTimeToSeconds = (time) => {
if (!time) return ''
const _date = new Date(time)
let year = _date.getFullYear()
let month = _date.getMonth() + 1
let day = _date.getDate()
let hour = _date.getHours()
let minute = _date.getMinutes()
let seconds = _date.getSeconds()
return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')} ${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
}
export default (Vue) => {
Vue.filter('formatPrice', formatPrice)
Vue.filter('formatDate', formatDate)
Vue.filter('formatTime', formatTimeToSeconds)
Vue.filter('formatTimeToSeconds', formatTimeToSeconds)
}
app.js:
import Vue from 'vue'
import registerFilter from './utils/filters'
registerFilter(Vue)
组件中使用:
<template>
<div>{{ price | formatPrice }}</div>
</template>
表格过滤组件
控制台项目里,最常见的就是查询记录以表格的形式展现出来,表格的功能大多也都比较类似,所以我们可以封装一个通用的表格组件,帮助我们简化一下表格的操作。
这个表格组件将包含以下功能:
- 多种数据来源:表格的数据可以由用户传入,也可以通过请求api获得数据
- 数据查询:可以支持常见的输入框、下拉框、时间选择器的筛选
- 分页:根据传入参数,动态决定每页展示数据量
- 格式化数据:根据传入的规则对查询获取的数据格式化
- 自定义表格内容:允许用户自由编辑表格内容
<template>
<div>
<el-form :inline="true" :model="filter" class="demo-form-inline">
<el-form-item v-for="item in filterItems" :key="item.prop" :label="item.label">
<el-date-picker
v-if="item.type === 'daterange'"
v-model="filter[item.prop]"
:default-time="['00:00:00', '23:59:59']"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期">
</el-date-picker>
<el-date-picker
v-else-if="item.type === 'date'"
v-model="filter[item.prop]"
type="date"
placeholder="选择日期">
</el-date-picker>
<el-select v-else-if="item.type === 'select'" v-model="filter[item.prop]" :placeholder="item.placeholder || item.label" clearable>
<el-option
v-for="option in item.options"
:key="option.value"
:label="option.label"
:value="option.value">
</el-option>
</el-select>
<el-input v-else v-model="filter[item.prop]" :placeholder="item.placeholder || item.label" :type="item.type" clearable></el-input>
</el-form-item>
<el-form-item v-if="filterItems && filterItems.length > 0">
<el-button type="primary" @click="refresh">查询</el-button>
</el-form-item>
<el-form-item v-if="filterItems && filterItems.length > 0">
<el-button @click="reset">重置条件</el-button>
</el-form-item>
</el-form>
<slot :data="list"></slot>
<div class="pagination">
<el-pagination
background
layout="total, prev, pager, next"
:current-page="page"
:page-size="rows"
:total="total"
@current-change="changePage"
></el-pagination>
</div>
</div>
</template>
<script>
export default {
name: 'TableFilter',
props: {
// 可选,表格数据
tableData: Array,
// 可选,请求api地址
url: String,
// 表格筛选项
filterItems: {
type: Array,
default () {
return []
}
},
// 筛选数据
filter: {
type: Object,
default () {
return {}
}
},
// 每页展示数据量
defaultRows: {
type: Number,
default: 10
},
// 格式化规则
formatTableData: Function
},
data () {
return {
defaultFilter: { ...this.filter },
list: [],
rows: this.defaultRows,
total: 0,
page: 1
}
},
watch: {
tableData: {
handler (tableData) {
this.calcTableData(tableData)
},
immediate: true
}
},
methods: {
reset () {
for (const key in this.filter) {
if (this.filter.hasOwnProperty(key)) {
this.filter[key] = this.defaultFilter[key]
}
}
},
changePage (page) {
this.page = page
this.search()
},
// 针对用户传入表格数据的情况做前端分页
calcTableData (tableData = []) {
const list = tableData.slice((this.page - 1) * this.rows, this.page * this.rows)
this.list = this.formatTableData ? this.formatTableData(list) : list
this.total = tableData.length
},
search () {
if (this.tableData) {
this.calcTableData(this.tableData)
} else {
// 发送请求
const filter = {}
Object.keys(this.filter).forEach(key => {
if (this.filter[key]) {
if (key === 'daterange') {
filter['startTime'] = this.filter[key][0]
filter['endTime'] = this.filter[key][1]
} else {
filter[key] = this.filter[key]
}
}
})
this.$http.get(this.url, { params: { ...filter, page: this.page, rows: this.rows } }).then(({ total, list }) => {
this.total = total
this.list = this.formatTableData ? this.formatTableData(list) : list
})
}
},
refresh () {
this.page = 1
this.search()
}
}
}
</script>
<style lang="scss" scoped>
</style>
下面我们来详细说下实现思路:
- 多种数据来源:
- 数据查询:
[
{ prop: 'name', type: 'text', label: '名称' },
{ prop: 'gender', type: 'select', label: '性别', options: [{ label: '男', value: 'male' }, { label: '女', value: 'female' }] }
]
- 分页:
- 格式化数据:
const formatItem = (item) => {
// do something...
}
const formatTableData = (list) => {
list.map(formatItem)
}
- 自定义表格内容:
<!-- 子组件:-->
<slot :data="list"></slot>
<!-- 插槽内容 -->
<template v-slot="{ data }"></template>
接下来看看在组件中使用的完整示例:
<template>
<div>
<TableFilter url="/getUser" :filterItem="filterItem" :filter="filter" :defaultRows="20" :formatTableData="formatTableData">
<template v-slot="{ data }">
<el-table :data="data">
<el-table-column prop="name" label="名称"></el-table-column>
<el-table-column prop="gender" label="性别"></el-table-column>
<el-table-column label="添加时间" sortable prop="createtime">
<template v-slot="{ row }">
<div >{{ row.createtime | formatTime }}</div>
</template>
</el-table-column>
<el-table-column label="操作">
<template v-slot="{ row }">
<el-link :underline="false" @click="toDetail(row.id)">查看</el-link>
</template>
</el-table-column>
</el-table>
<template>
</TableFilter>
</div>
</template>
<script>
const formatItem = (item) => {
// do something
}
export default {
data () {
return {
filterItem: [
{ prop: 'name', type: 'text', label: '名称' },
{ prop: 'gender', type: 'select', label: '性别', options: this.genderOptions }
],
genderOptions: [{ label: '男', value: 'male' }, { label: '女', value: 'female' }],
filter: {
status: 'enable'
}
}
},
methods: {
formatTableData (list) {
list.map(formatItem)
}
}
}
</script>
单例插件
项目中存在一类组件,这类组件可能是个在页面中会被频繁调用的弹出框,对于这类组件,显然在页面中引入多个是个不明智的做法,所以大多时候,我们会引入一个,让他根据不同的交互场景重新渲染内容,但是更好的做法是将他做成一个单例插件,通过函数调用的方法使用。
这里将介绍两种方法:
- 封装成vue插件,全局引入
- 单独引入,函数式调用
两种方法在使用上其实相差无几,在项目中,可以根据自己的喜好任选一种。
vue插件:
目录结构:
├── SingleComponent
├── component.vue # 组件内容
└── index.js # 组件创建
index.js:
import component from './component.vue'
let SingleComponent = {
install: function (Vue) {
const Constructor = Vue.extend(component)
const instance = new Constructor()
instance.init = false
Vue.prototype.$singleComponent = (options, callback) => {
if (!instance.init) {
instance.$mount()
document.body.appendChild(instance.$el)
instance.init = true
}
// 从options里获取参数,赋值给组件实例中的data
// 传入的callback绑定给组件实例的某个方法,实例方法将会把组件的数据暴露给这个回调
instance.someOption = options.someValue
instance.someMethods = callback
}
}
}
export default SingleComponent
app.js:
import Vue from 'vue'
import SingleComponent from './global/SingleComponent'
Vue.use(SingleComponent)
组件内使用:
export default {
data () {
return {
studentsList: []
}
}
methods: {
useComponent () {
this.$singleComponent({ gender: 'male', age: 12 }, (list) => {
this.studentsList = list
})
}
}
}
函数式组件:
目录结构:
├── SingleComponent
├── component.vue # 组件内容
└── index.js # 组件创建
index.js:
import Vue from '@/app.js'
import Component from './component.vue'
let instance = null
let SingleComponent = (options, callback) => {
if (!instance) {
const Consturctor = Vue.extend(Component)
instance = new Consturctor()
instance.$mount()
document.body.appandchild(instance.$el)
}
// 从options里获取参数,赋值给组件实例中的data
// 传入的callback绑定给组件实例的某个方法,实例方法将会把组件的数据暴露给这个回调
instance.someOption = options.someValue
instance.someMethods = callback
return instance
}
组件中使用:
import SingleComponent from '@/global/SingleComponent'
export default {
data () {
return {
studentsList: []
}
},
methods: {
useComponent () {
SingleComponent({ gender: 'male', age: 12 }, (list) => {
this.studentsList = list
})
}
}
}
CMS组件
随着一个项目的发展,我们必然会遇到一些特殊的页面,这些页面在不同的版本中,可能会频繁修改布局内容,如果按照传统的固定页面开发模式,将增加大量的工作量,所以CMS应运而生。
CMS(Content Management System)即内容管理系统,它的作用是可以让一个即使没有编码能力的用户,通过可视化的操作,就能编写出一个自定义的页面。
这里的示例,部分将会使用伪代码,主要提供一个CMS系统的设计思路。
目录结构:
├── CMS
├── components # 存放公用组件
├── Modules # CMS组件库
│ ├── Text # 示例文本组件
│ │ ├── Module.vue # 预览模块
│ │ ├── options.js # 可修改配置属性及校验
│ │ └── Options.vue # 配置属性操作面板
│ └── index.js # 注册CMS组件
└── index.vue # 主面板
自选组件
从前面的目录结构,我们已经可以看出来,一个自选模块我们将用三个文件来实现:
- 预览模块:根据配置最终将展示的组件成品
- 属性及校验:定义组件将使用的属性结构及保存所需的校验
- 属性操作面板:包含所有可配置属性的表单
这里我们以一个文字组件作为示例,我们先来看看其中最关键的属性结构和校验的定义:
options.js:
import Validator from 'async-validator'
export const name = '文本' // 自选组件名称
// 校验规则
const descriptor = {
target: {
type: 'object',
fields: {
link: { type: 'string', required: false },
name: { type: 'string', required: false }
}
},
text: { type: 'string', required: true, message: `${name}组件,内容不能为空` }
}
const validator = new Validator(descriptor)
export const validate = (obj) => {
return validator.validate(obj)
}
// 默认属性
export const defaultOptions = () => {
return {
target: {
link: '',
name: ''
},
text: '',
color: 'rgba(51, 51, 51, 1)',
backgroundColor: 'rgba(255, 255, 255, 0)',
fontSize: '16px',
align: 'left',
fontWeight: 'normal'
}
}
对属性的定义有了了解后,我们来看看对应的操作面板:
Options.vue:
<template>
<el-form label-width="100px" :model="form">
<el-form-item label="文本:" prop="text">
<el-input type="textarea" :rows="3" v-model="form.text"></el-input>
</el-form-item>
<el-form-item label="字体大小:" class="inline">
<el-input v-model.number="fontSize"></el-input>px
</el-form-item>
<el-form-item label="字体颜色:">
<el-color-picker v-model="form.color" show-alpha></el-color-picker>
</el-form-item>
<el-form-item label="背景颜色:">
<el-color-picker v-model="form.backgroundColor" show-alpha></el-color-picker>
</el-form-item>
<el-form-item label="字体加粗:">
<el-checkbox v-model="checked"></el-checkbox>
</el-form-item>
<el-form-item label="对齐方式:">
<el-radio-group v-model="form.align">
<el-radio label="left">左对齐</el-radio>
<el-radio label="center">居中对齐</el-radio>
<el-radio label="right">右对齐</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</template>
<script>
import { defaultOptions } from './options'
export default {
name: 'options',
props: {
form: {
type: Object,
default () {
return defaultOptions()
}
}
},
watch: {
fontSize (val) {
this.fontSize = val.replace(/[^\d]/g, '')
}
},
computed: {
// 字体大小
fontSize: {
get () {
return this.form.fontSize.slice(0, -2)
},
set (val) {
this.form.fontSize = val + 'px'
}
},
// 字体是否加粗
checked: {
get () {
return this.form.fontWeight === 'bold'
},
set (val) {
if (val) {
this.form.fontWeight = 'bold'
} else {
this.form.fontWeight = 'normal'
}
}
}
},
data () {
return {
}
}
}
</script>
实际上,每个自选组件的配置属性,都将保存在form属性中,之后他会作为prop属性传给Module以展示预览效果。
Module.vue:
<template>
<div class="text" :style="style">
{{options.text}}
</div>
</template>
<script>
export default {
name: 'module',
props: {
options: {
type: Object,
default () {
return {
text: '',
align: 'left',
color: 'rgba(19, 206, 102, 0.8)',
backgroundColor: 'rgba(255, 255, 255, 0)',
fontSize: '16px',
fontWeight: 'normal'
}
}
}
},
computed: {
style () {
return {
textAlign: this.options.align,
color: this.options.color,
backgroundColor: this.options.backgroundColor,
fontSize: this.options.fontSize,
fontWeight: this.options.fontWeight
}
}
},
data () {
return {
}
}
}
</script>
<style lang="css" scoped>
.text {
word-break: break-all;
}
</style>
光看自选组件的三个文件,我们好像并没有将他们串在一起,别急,这些我们最终会在主面板里实现。
自选组件注册入口
index.js:
// 获取需要引入的自选组件
export const getComponents = () => {
const modules = require.context('./', true, /.vue$/)
const components = {}
modules.keys().map(fileName => {
const componentName = fileName.replace(/\.\/(\w+)\/(\w+).vue$/, '$1$2')
components[componentName] = modules(fileName).default
})
return components
}
// 获取自选组件的预览模块
export const getModules = () => {
const modules = require.context('./', true, /.vue$/)
const cells = modules.keys().map(fileName => {
return fileName.replace(/\.\/(\w+)\/\w+.vue$/, '$1')
})
return Array.from(new Set(cells))
}
// 获取自选组件默认属性
export const getDefaultOptions = () => {
const modules = require.context('./', true, /options.js$/)
const ret = {}
modules.keys().forEach(fileName => {
ret[fileName.replace(/\.\/(\w+)\/\w+.js$/, '$1')] = modules(fileName).defaultOptions
})
return ret
}
// 获取自选组件校验函数
export const getValidates = () => {
const modules = require.context('./', true, /options.js$/)
const ret = {}
modules.keys().forEach(fileName => {
ret[fileName.replace(/\.\/(\w+)\/\w+.js$/, '$1')] = modules(fileName).validate
})
return ret
}
// 获取自选组件名称
export const getModuleName = () => {
const modules = require.context('./', true, /options.js$/)
const ret = {}
modules.keys().forEach(fileName => {
ret[fileName.replace(/\.\/(\w+)\/\w+.js$/, '$1')] = modules(fileName).name
})
return ret
}
在index.js中定义的几个函数,都将在主面板中使用。
主面板
页面主要分为这样几个区块:
- 自选组件列表
- 已添加组件列表操作面板
- 预览区域
- 详情操作面板
示例图:
现在我们来看看主面板的实现:
<template>
<div class="manage-content">
<div class="designer">
<div class="designer-menus__left">
<div class="label">组件列表:</div>
<span class="cell" v-for="cell in cells" :key="cell" @click="addModule(cell)">{{nameMap[cell]}}</span>
<div class="label">页面导航:</div>
<div v-if="modules.length === 0" class="map-wrapper">
<div class="map-module">
未添加组件
</div>
</div>
<draggable v-else v-model="modules" class="map-wrapper" handle=".el-icon-rank">
<div v-for="module in modules" class="map-module" :class="{ select: module.id === curModule.id }" :key="module.id" @click="selModule(module)">
<i class="el-icon-rank"></i>
<div class="name">
{{nameMap[module.type]}}
</div>
<i class="el-icon-close" @click.stop="delModule(module.id)"></i>
</div>
</draggable>
</div>
<div class="designer-content">
<!-- 预览区域 -->
<div class="screen" ref="screen">
<div class="module" v-for="module in modules" :key="module.id" @click="selModule(module)" :class="{ select: module.id === curModule.id }" :id="module.id">
<component :is="module.type + 'Module'" :options="module.options"></component>
</div>
</div>
<!-- 操作区域 -->
<div class="operation-content">
<el-button @click="$router.back()">取消</el-button>
<el-button @click="save">保存</el-button>
</div>
</div>
<div class="designer-menus__right">
<!-- tab栏,配置组件和页面 -->
<el-tabs v-model="activeName" type="card">
<el-tab-pane label="组件管理" name="module">
<component v-if="curModule.type" :is="curModule.type + 'Options'" :form="curModule.options"></component>
</el-tab-pane>
<el-tab-pane label="页面管理" name="page">
<!-- 页面全局信息配置,因为不是重点所以这里就不具体展示 -->
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
</template>
<script>
import { v1 } from 'uuid'
import draggable from 'vuedraggable'
import { getModules, getDefaultOptions, getComponents, getModuleName, getValidates } from './Modules'
const validates = getValidates()
const defaultOptions = getDefaultOptions()
export default {
name: 'Designer',
components: { ...getComponents(), draggable },
props: {
// 页面信息,用于回显
pageForm: {
type: Object
}
},
data () {
return {
nameMap: getModuleName(),
cells: getModules(),
modules: [],
curModule: {
type: ''
},
activeName: 'module' // tab激活页
}
},
created () {
this.resumePage() // 检测是否需要回填数据
},
methods: {
// 回填数据
resumePage () {
if (this.pageForm) {
let page = JSON.parse(this.pageForm.page)
this.modules = page.modules
if (this.modules.length > 0) {
this.curModule = this.modules[0]
}
}
},
selModule (module) {
this.curModule = module
const elem = document.getElementById(module.id)
this.$refs.screen.scrollTo(0, elem.offsetTop)
},
delModule (id) {
const index = this.modules.findIndex(({ id: _id }) => id === _id)
this.modules.splice(index, 1)
this.curModule = this.modules.length > 0 ? this.modules[index > 0 ? index - 1 : index] : { type: '' }
},
addModule (module) {
const id = v1()
this.modules.push({ id, type: module, options: defaultOptions[module]() })
this.curModule = this.modules[this.modules.length - 1]
this.$nextTick(() => {
const elem = document.getElementById(id)
this.$refs.screen.scrollTo(0, elem.offsetTop)
})
},
// 保存
save () {
let pageContent = {
modules: this.modules
}
let form = {
page: JSON.stringify(pageContent)
}
// 校验组件数据
const promises = this.modules.map(({ type, options }) => {
return validates[type](options)
})
Promise.all(promises).then(data => {
// submit form
}).catch(({ error, fields }) => {
const [{ message }] = Object.values(fields)[0]
this.$message.error(message)
})
}
}
}
</script>
这里比较关键的一点是,因为我们将会频繁对自选组件进行增删改,预览区域渲染的自选组件和详情操作面板中的内容将会经常变换,所以我们可以使用动态组件<component>
结合is
属性的绑定来实现。
写在最后
这里分享的实践只是一部分,也并不一定是最佳的,所以如果有更加好的解决方法也欢迎大家在评论里补充。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!