【珍惜时间】iReport

项目很点意思,感觉很高超的样子
先放下项目的github地址:https://github.com/tctangyanan/iReport
感谢各位伟大的程序员无私的分享自己的技术
老规矩,我们会运行项目
页面效果为

先逐行分析代码
先看main.js,引入了我们需要的一些全局插件

//main.js
import Vue from 'vue'
import 'normalize.css/normalize.css'// A modern alternative to CSS resets
import App from './App'
import router from './router'
// import routes from './router/routes'
import store from './vuex/store'
import { sync } from 'vuex-router-sync'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import './styles/index.scss' // global css
// 进度条
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'// progress bar style
import UtilsPlugin from './assets/utils'
import HttpPlugin from './http/index'
import VueParticles from 'vue-particles' // 粒子酷炫效果
import formatStyle from './filters/formatStyle.js'
// import { Tag } from '../src/model/index'
import './mock' // mock
import VCharts from 'v-charts'
Vue.config.productionTip = false
Vue.use(ElementUI)
// plugins
Vue.use(UtilsPlugin)
Vue.use(HttpPlugin)
Vue.use(VCharts)
Vue.use(VueParticles)
Vue.filter('formatStyle', formatStyle)
// const dispatch = store.dispatch
router.beforeEach((to, from, next) => {
  NProgress.start() // start progress bar
  if (to.path === '/' || to.path === '/login' || to.path === '/404' || to.path === '/401') {
    setTimeout(next, 0)
  } else {
    setTimeout(next, 20)
  }
})
router.afterEach((to) => {
  NProgress.done() // finish progress bar
})
sync(store, router)
/* eslint-disable no-new */
// global
window.$globalHub = new Vue({
  store,
  router,
  render: h => h(App)
}).$mount('#app')

//App.vue
<template>
  <div id="app" style="height:100%;">
    <!--default slot-->
    <router-view :key="$route.path"></router-view>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  components: {},
  data () {
    return {}
  },
  mounted () {
    // 禁用整个页面的右击事件
    // document.oncontextmenu = () => {
    //   return false
    // }
  },
  computed: {
    ...mapState({})
  },
  methods: {}
}
</script>

<style lang="less" rel="stylesheet/less">
#nprogress .spinner {
  display: none;
}

@import "styles/icon.css";
</style>

接下来看路由页面

//router.js
/* eslint-disable no-undef */
import NotFind from '../pages/errors/404.vue'
import VChars from '../pages/test/VChars.vue'
// 生产/测试环境,使用路由懒加载
const _import = process.env.NODE_ENV === 'development'
  ? file => require(`@/pages/${file}.vue`).default
  : file => () => System.import(`@/pages/${file}.vue`).then(m => m.default)
export default [
  { path: '/', component: _import('login/Index') },
  { path: '/login', component: _import('login/Index') },
  { path: '/VChars', component: VChars },
  {
    path: '/main',
    component: resolve => require(['../layout/Layout'], resolve),
    children: [
      {
        path: '/dashboard',
        name: '首页',
        component: _import('dashboard/Index')
      }, {
        path: '/table/list',
        name: '表格',
        component: _import('table/Index')
      }, {
        path: '/icons',
        name: '图标',
        component: _import('icons/Index')
      }, {
        path: '/role/list',
        name: '角色管理',
        component: _import('roles/Index')
      }, {
        path: '/user/list',
        name: '用户管理',
        component: _import('users/Index')
      }, {
        path: '/menus/list',
        name: '菜单设置',
        component: _import('menus/Index')
      }, {
        path: '/smsCode/list',
        name: '短信码',
        component: _import('smsCode/Index')
      }, {
        path: '/errorCode/list',
        name: '错误码',
        component: _import('errorCode/Index')
      }, {
        path: '/404',
        name: '404',
        component: NotFind
      }, {
        path: '/401',
        name: '401',
        component: _import('errors/401')
      }
    ]
  },
  { path: '/editor', component: _import('editor/Index') }
]
//index.js
/* eslint-disable no-undef */
import Vue from 'vue'
import Router from 'vue-router'
import config from '../../config/index'
import routes from './routes'

Vue.use(Router)
export default new Router({
  mode: 'history', // 后端支持可开
  base: config.build.assetsPublicPath,
  routes
})

接下来我们根据页面效果看代码

//srcpagesloginIndex.vue
<template>
  <!--region 粒子效果的登录背景-->
  <div class="login-page">
    <vue-particles color="#fff" :particleOpacity="0.7" :particlesNumber="60" shapeType="circle" :particleSize="4"
                   linesColor="#fff" :linesWidth="1" :lineLinked="true" :lineOpacity="0.4" :linesDistance="150"
                   :moveSpeed="2" :hoverEffect="true" hoverMode="grab" :clickEffect="true" clickMode="push"
                   class="bg-lizi">
    </vue-particles>
    <!--region 登录表单-->
    <el-form :model="loginForm" class="login-form" label-position="left" size="large">
      <el-form-item>
        <el-input type="text" v-model="loginForm.phone" auto-complete="off" placeholder="注册时的手机号">
          <span v-html="'&#xe851;'" class="axon-icon" slot="prefix"></span>
        </el-input>
      </el-form-item>
      <el-form-item>
        <el-input :type="passwordType" v-model="loginForm.password" size="large" auto-complete="off" placeholder="登录密码">
          <span v-html="'&#xe62a;'" class="axon-icon" slot="prefix"></span>
          <span v-html="passwordType === 'password' ? '&#xe901;' : '&#xe6e0;'" class="axon-icon" slot="suffix"
                @click="showPwd()"></span>
        </el-input>
      </el-form-item>
      <el-button type="primary" @click="submitLogin" @enter="submitLogin()" :loading="loading">登&nbsp;&nbsp;&nbsp;&nbsp;录</el-button>
    </el-form>
    <!--endregion-->
  </div>
  <!--endregion-->
</template>
<script>
import BLL from './Index.js'
import menusLisit from '../../../static/json/limit.json'

export default {
  components: {},
  data () {
    return {
      loginForm: {
        phone: '18951871658',
        password: '123456'
      },
      loading: false,
      menus: menusLisit,
      passwordType: 'password' // 密码控件的类型
    }
  },
  created () {
    this.BLL = new BLL(this)
  },
  mounted () {
  },
  methods: {
    // 登录
    submitLogin () {
      this.BLL.login()
    },
    showPwd () {
      this.passwordType === 'password' ? this.passwordType = 'text' : this.passwordType = 'password'
    }
  }
}
</script>

<style lang="scss">
  @import "./Index.scss";
</style>
//srcpagesloginIndex.js
import Base from '../../base/index'

export default class extends Base {
  /**
   * 用户登录
   */
  login () {
    if (!this.vm.loginForm.phone) {
      this.vm.$message.warning('请填写登录账号!')
      return false
    }
    if (!this.vm.$utils.Validate.chkFormat(this.vm.loginForm.phone, 'phone')) {
      this.vm.$message.warning('登录手机号的格式不正确!')
      return false
    }
    if (!this.vm.loginForm.password) {
      this.vm.$message.warning('请填写登录密码!')
      return false
    }
    if (this.vm.loginForm.phone !== '18951871658' || this.vm.loginForm.password !== '123456') {
      this.vm.$message.warning('账号或密码不匹配!')
      return false
    }
    this.vm.loading = true
    // 取用户的平台权限信息,并且持久化
    this.vm.$store.dispatch('init_sidebar_data', this.vm.menus)
    // 清空tab标签
    this.vm.$store.dispatch('del_all_tags')
    // 登录成功
    this.vm.$notify.success({
      title: '登录成功',
      message: '欢迎小主回来!'
    })
    this.vm.$router.push({path: '/dashboard'})
  }
}

//srcpagesdashboardIndex.vue
<template>
  <div class="dashboard-page">
    <!--region 栏目-->
    <el-row class="panel-group" :gutter="40">
      <template v-for="(panel, index) in panelList">
        <el-col :key="index" :xs="12" :sm="12" :lg="6" class="card-panel-col">
          <div class='card-panel' @click="handleSetLineChartData(panel.id)">
            <div class="card-panel-icon-wrapper icon-people">
              <div :class="`card-panel-icon panel-${panel.color}`">
                <span class="axon-icon" v-html="panel.icon"></span>
              </div>
            </div>
            <div class="card-panel-description">
              <div class="card-panel-text">{{ panel.label }}</div>
              <count-to class="card-panel-num" :startVal="0" :endVal="panel.value" :decimals=2 :duration="2600"></count-to>
            </div>
          </div>
        </el-col>
      </template>
    </el-row>
    <!--endregion-->
    <!--region 创建新场景-->
    <div class="add-new-scene">
      <el-button type="primary" icon="el-icon-plus" plain round @click.native="creatNewScene">添加新场景</el-button>
    </div>
    <!--endregion-->
  </div>
</template>
<script>
const CountTo = () => import('vue-count-to')
const lineChartData = {
  newVisitis: {
    expectedData: [100, 120, 161, 134, 105, 160, 165],
    actualData: [120, 82, 91, 154, 162, 140, 145]
  },
  messages: {
    expectedData: [200, 192, 120, 144, 160, 130, 140],
    actualData: [180, 160, 151, 106, 145, 150, 130]
  },
  purchases: {
    expectedData: [80, 100, 121, 104, 105, 90, 100],
    actualData: [120, 90, 100, 138, 142, 130, 130]
  },
  shoppings: {
    expectedData: [130, 140, 141, 142, 145, 150, 160],
    actualData: [120, 82, 91, 154, 162, 140, 130]
  }
}
export default {
  components: { CountTo },
  data () {
    return {
      panelList: [
        {
          id: 'newVisitis',
          label: 'Demo1',
          value: 102400,
          icon: '&#xe63e;',
          color: 1// '#40c9c6'
        },
        {
          id: 'messages',
          label: 'Demo2',
          value: 81212,
          icon: '&#xe72d;',
          color: 2 // '#36a3f7'
        },
        {
          id: 'purchases',
          label: 'Demo3',
          value: 9280.3,
          icon: '&#xe736;',
          color: 3 // '#f4516c'
        },
        {
          id: 'shoppings',
          label: 'Demo4',
          value: 13600.6,
          icon: '&#xe61b;',
          color: 4 // '#34bfa3'
        }
      ],
      lineChartData: lineChartData.newVisitis
    }
  },
  mounted () {
  },
  methods: {
    handleSetLineChartData (panel) {
      this.lineChartData = lineChartData[panel]
    },
    /**
     * 创建新场景
     */
    creatNewScene () {
      this.$router.push({ path: '/editor' })
    }
  }
}
</script>

<style lang="scss" scoped>
@import "../../styles/font.scss";

.dashboard-page {
  background-color: #f0f2f5;
  padding: 32px;
  overflow-y: auto;
  .panel-group {
    padding: 40px 0;
    .card-panel-col {
      margin-top: 10px;
      .card-panel {
        height: 108px;
        cursor: pointer;
        font-size: $font-size-s;
        position: relative;
        overflow: hidden;
        color: #666;
        background: #fff;
        -webkit-box-shadow: 4px 4px 40px rgba(0, 0, 0, 0.05);
        box-shadow: 4px 4px 40px rgba(0, 0, 0, 0.05);
        border-color: rgba(0, 0, 0, 0.05);
        display: flex;
        padding: 0 20px;

        &:hover {
          .card-panel-icon-wrapper {
            .card-panel-icon {
              border-radius: 4px;
              transition: all 0.38s ease-out;
              &.panel-1 {
                background-color: #40c9c6;
                .axon-icon {
                  color: #ffffff;
                }
              }
              &.panel-2 {
                background-color: #36a3f7;
                .axon-icon {
                  color: #ffffff;
                }
              }
              &.panel-3 {
                background-color: #f4516c;
                .axon-icon {
                  color: #ffffff;
                }
              }
              &.panel-4 {
                background-color: #34bfa3;
                .axon-icon {
                  color: #ffffff;
                }
              }
            }
          }
        }
        .card-panel-icon-wrapper {
          flex: 0 0 50%;
          .card-panel-icon {
             60px;
            height: 60px;
            margin-top: 29px;
            text-align: center;
            line-height: 60px;
            &.panel-1 {
              .axon-icon {
                color: #40c9c6;
              }
            }
            &.panel-2 {
              .axon-icon {
                color: #36a3f7;
              }
            }
            &.panel-3 {
              .axon-icon {
                color: #f4516c;
              }
            }
            &.panel-4 {
              .axon-icon {
                color: #34bfa3;
              }
            }
            .axon-icon {
              font-size: $font-size-large;
            }
          }
        }
        .card-panel-description {
          flex: 0 0 50%;
          text-align: right;
          margin: 26px 0;
          .card-panel-text {
            font-size: $font-size-xxl;
            font-weight: 600;
            line-height: 32px;
            color: rgba(0, 0, 0, 0.45);
          }
          .card-panel-num {
            font-weight: 600;
            font-size: $font-size-xxxl;
          }
        }
      }
    }
  }
  .add-new-scene {
    text-align: center;
  }
}
</style>

这个里面写的css的方式处理的非常优雅,包括在css里面的定义,很值得学习
接下来的页面是,点击添加新场景


我们先看Index.vue中引入的组件

//srcpageseditorIndex.vue
<template>
  <div class="editor-index">
    <!--顶部各种组件列表-->
    <the-tools></the-tools>
    <!--region 中间区域分为三个区间-->
    <div class="index-center" ref="centerBox">
      <!--左边在线模板列表-->
      <the-template-list></the-template-list>
      <!--内容面板 编辑-->
      <div class="editor-box">
        <the-editor-container ref="editorContainer"></the-editor-container>
      </div>
      <!--右侧页面列表-->
      <the-page-list></the-page-list>
      <!--组件属性设置面板-->
      <the-comp-props-config></the-comp-props-config>
      <!--组件右击图层设置面板-->
      <the-comp-layer-manager></the-comp-layer-manager>
    </div>
    <!--endregion-->
  </div>
</template>

<script>
// 右侧页面列表
import ThePageList from './components/ThePageList'
// 左边在线模板列表
import TheTemplateList from './components/TheTemplateList'
// 顶部各种组件列表
import TheTools from './components/TheTools'
// 面板
import TheEditorContainer from './components/TheEditorContainer'
// 组件属性设置面板
import TheCompPropsConfig from './components/TheCompPropsConfig'
// 组件右击图层设置面板
import TheCompLayerManager from './components/TheCompLayerManager'

export default {
  // 引入组件
  components: {
    ThePageList,
    TheTemplateList,
    TheTools,
    TheEditorContainer,
    TheCompPropsConfig,
    TheCompLayerManager
  },
  data () {
    return {
      toolJson: {},
      panelWidthAndHeight: {
         200,
        height: 400
      }
    }
  },
  created () {
    // 初始化
    this.$store.dispatch('initPageEditor')
  },
  mounted () {
  },
  computed: {},
  methods: {}
}
</script>

<style lang="scss">
@import "../../styles/variables";
@import "../../styles/mixin";

.editor-index {
  background-color: #fff;
   100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  .index-center {
    box-sizing: border-box;
    flex: 0 0 calc(#{"100% - "+ $tool-header-height +""});
    display: flex;
    flex-direction: row;
    position: relative;
    @include scrollBar;
    .editor-box {
      box-sizing: border-box;
       100%;
      overflow-y: auto;
      padding: 40px 0;
      background-color: #eee;
    }
  }
}
</style>

接下来我们一个一个分析

//srccomponentsEChars
eportComponent.js

export default {
  // 园区客流检测 数据
  reportComponentList: [
    {
      componentId: 87001,
      componentName: '柱状图',
      sourceRemark: '项王故里为节日期间游客量最多的,三台山森林公园和洪泽湖湿地公园分列二三名。',
      tempHtml: '<i-LBarFour :chartsId="dataObj.chartsId" :title="dataObj.title" :xAxis="dataObj.xAxis" :yAxis="dataObj.yAxis" :unit="dataObj.unit"></i-LBarFour >',
      dataObj: {
        chartsId: `circularCharsID`,
        title: '接待省外访客量TOP10景区排名',
        xAxis: ['项王故里', '三台山森林公园', '洪泽湖湿地公园', '湖滨公园', '洋河酒厂', '雪枫公园', '龙王庙行宫', '中国杨树博物馆'],
        yAxis: [38056, 33824, 22672, 18616, 13112, 7016, 6864, 5792],
        unit: '人次'
      }
    },
    {
      componentId: 87002,
      componentName: '折线图',
      sourceRemark: '中秋期间,9月23日接待访客量最多,9月24日接待访客量最少',
      tempHtml: '<i-Line :chartsId="dataObj.chartsId" :title="dataObj.title" :xAxis="dataObj.xAxis" :yAxis="dataObj.yAxis" :unit="dataObj.unit"></i-Line >',
      dataObj: {
        chartsId: `lineCharsID`,
        title: '分日客流量变化',
        xAxis: ['9月22日', '9月23日', '9月23日'],
        yAxis: [59.28, 63.86, 33.86],
        unit: '万人'
      }
    },
    {
      componentId: 87003,
      componentName: '饼状图',
      sourceRemark: '节假日期间,年龄在22岁及以下、23-35岁和36-45岁的访客占访客总量的60%',
      tempHtml: '<i-HollowPie :chartsId="dataObj.chartsId" :title="dataObj.title" :xAxis="dataObj.xAxis" :yAxis="dataObj.yAxis" :unit="dataObj.unit"></i-HollowPie>',
      dataObj: {
        chartsId: `hollowPieCharsID`,
        title: '访客年龄段分布',
        xAxis: ['22岁及以下', '23-35岁', '36-45岁', '46-55岁', '56岁及以上'],
        yAxis: [25145, 108532, 83761, 91837, 53111],
        unit: '%'
      }
    }
  ]
}

//srccomponentsiDialogImgIndex.vue
<!--region 封装的图片列表 卡片-->
<template>
  <div class="custom-dialog-wrapper" v-if="dialogVisible">
    <div class="custom-dialog-container" :style="{ '60%',marginTop: '15vh'}">
      <el-container>
        <el-aside :style="{height: '75vh','200px'}">
          <div class="custom-aside__wrapper">
            <span class="custom-aside__title">图片库</span>
            <el-upload
              class="upload-demo"
              drag
              action="https://jsonplaceholder.typicode.com/posts/"
              multiple>
              <i class="el-icon-upload"></i>
              <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
              <div class="el-upload__tip" slot="tip">只能上传jpg/png文件,且不超过500kb</div>
            </el-upload>
          </div>
        </el-aside>
        <el-container>
          <el-header>
            <div class="custom-dialog__header">
              <span class="custom-dialog__title">{{title}}</span>
              <a class="custom-dialog__headerbtn" @click="closeDialog">
                <i class="el-dialog__close el-icon el-icon-close"></i>
              </a>
            </div>
          </el-header>
          <el-main>
            <ul v-if="operationFlag==='BackgroundImage'">
              背景图片
              <template v-for="item in backgroundImageList">
                <li :key="item.id" @click="selectImage(item)" style="height: 30vh;">
                  <div class="item-bg-images" :style="{backgroundImage: 'url(' + item.url + ')'}">
                    <div class="item-active"
                         v-if="imageSelectItem && imageSelectItem.id === item.id">
                      <span class="axon-icon" v-html="'&#xe61e;'"></span>
                    </div>
                  </div>
                </li>
              </template>
            </ul>
            <ul v-if="operationFlag==='Image' || operationFlag==='UpdateImages'">
              <template v-for="item in iconList">
                <li :key="item.id" @click="selectImage(item)" style="height: 24vh;border: 1px solid #c8c9ca">
                  <div class="item-bg-images" :style="{backgroundImage: 'url(' + item.url + ')'}">
                    <div class="item-active"
                         v-if="imageSelectItem && imageSelectItem.id === item.id">
                      <span class="axon-icon" v-html="'&#xe61e;'"></span>
                    </div>
                  </div>
                </li>
              </template>
            </ul>
          </el-main>
          <el-footer>
            <el-button @click="cancelSelectImage" round>&nbsp;&nbsp;&nbsp;&nbsp;取消&nbsp;&nbsp;&nbsp;&nbsp;</el-button>
            <el-button type="success" @click="confirmSelectImage" round>&nbsp;&nbsp;&nbsp;&nbsp;确定&nbsp;&nbsp;&nbsp;&nbsp;</el-button>
          </el-footer>
        </el-container>
      </el-container>
    </div>
  </div>
</template>
<!--endregion-->
<script>
export default {
  name: 'iDialogImg',
  data () {
    return {
      dialogVisible: '',
      title: '',
      operationFlag: '',
      backgroundImageList: [
        { id: 1, url: require('../../assets/images/bgi/1521186133451071.jpg') },
        { id: 2, url: require('../../assets/images/bgi/1522660019106280.png') },
        { id: 3, url: require('../../assets/images/bgi/1521186133451071.jpg') },
        { id: 4, url: require('../../assets/images/bgi/1522660019106280.png') },
        { id: 5, url: require('../../assets/images/bgi/1521186133451071.jpg') },
        { id: 6, url: require('../../assets/images/bgi/1522660019106280.png') },
        { id: 7, url: require('../../assets/images/bgi/1521186133451071.jpg') },
        { id: 8, url: require('../../assets/images/bgi/1522660019106280.png') },
        { id: 9, url: require('../../assets/images/bgi/1521186133451071.jpg') },
        { id: 10, url: require('../../assets/images/bgi/1522660019106280.png') },
        { id: 11, url: require('../../assets/images/bgi/1521186133451071.jpg') },
        { id: 12, url: require('../../assets/images/bgi/1522660019106280.png') },
        { id: 13, url: require('../../assets/images/bgi/1521186133451071.jpg') },
        { id: 14, url: require('../../assets/images/bgi/1522660019106280.png') }
      ],
      iconList: [
        { id: 1, url: require('../../assets/icon/bridge1.png') },
        { id: 2, url: require('../../assets/icon/person.png') },
        { id: 3, url: require('../../assets/icon/ren1.png') },
        { id: 4, url: require('../../assets/icon/bridge1.png') },
        { id: 5, url: require('../../assets/icon/person.png') },
        { id: 6, url: require('../../assets/icon/ren1.png') },
        { id: 7, url: require('../../assets/icon/bridge1.png') },
        { id: 8, url: require('../../assets/icon/person.png') },
        { id: 9, url: require('../../assets/icon/bridge1.png') },
        { id: 10, url: require('../../assets/icon/person.png') },
        { id: 11, url: require('../../assets/icon/person.png') },
        { id: 12, url: require('../../assets/icon/ren1.png') },
        { id: 13, url: require('../../assets/icon/bridge1.png') },
        { id: 14, url: require('../../assets/icon/person.png') }
      ],
      imageSelectItem:
        {
          id: null,
          url:
            ''
        }
    }
  },
  created () { },
  mounted () {
  },
  computed: {
    // 取当前页面
    curPage () {
      return this.$store.getters.curPage
    },
    // 取当前组件
    currComp () {
      return this.$store.getters.curComp
    }
  },
  methods: {
    // 关闭弹出框
    closeDialog () {
      this.dialogVisible = false
    },
    // 新增图片/背景图增弹出框
    showAddDialog (flag) {
      this.operationFlag = flag
      switch (flag) {
        case 'Image':
          this.title = '新增图标'
          break
        case 'BackgroundImage':
          this.title = '设置背景图'
          break
        default:
          break
      }
      this.dialogVisible = true
    },
    // 编辑图片/背景图增弹出框
    showEditDialog () {
      this.title = '修改图标'
      this.operationFlag = 'UpdateImages'
      this.dialogVisible = true
    },
    // 取消图片选中
    cancelSelectImage () {
      this.imageSelectItem.id = null
      this.imageSelectItem.url = ''
    },
    // 确认操作
    confirmSelectImage () {
      this.closeDialog()
      switch (this.operationFlag) {
        case 'Image':
          this.handleAddImageComponent(this.imageSelectItem)
          break
        case 'BackgroundImage':
          this.updateCurPageBackgroundImage(this.imageSelectItem)
          break
        case 'UpdateImages':
          this.updateProps()
          break
        default:
          break
      }
    },
    // 选中事件
    selectImage (item) {
      console.log(item)
      this.imageSelectItem.id = item.id
      this.imageSelectItem.url = item.url
    },
    // 添加图片
    handleAddImageComponent (item) {
      this.$store.dispatch('addNewCompByParams', { name: 'Image', url: item.url })
    },
    // 编辑图片组件
    updateProps () {
      this.$store.dispatch('editComp', {
        type: 'props',
        value: { 'url': this.imageSelectItem.url }
      })
    },
    // 设置当前页面背景
    updateCurPageBackgroundImage (item) {
      this.curPage.css.bgi = item.url
      this.syncCss('css', { 'bgi': this.curPage.css['bgi'] }, this.curPage.id)
    },
    // 同步到持久化
    syncCss (type, val, pageId) {
      this.$store.dispatch('editCurPage', {
        type: type,
        value: val,
        pageId: pageId
      })
    }
  }
}
</script>

<style lang="scss">
.custom-dialog-wrapper {
  z-index: 2025;
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  overflow: auto;
  margin: 0;
  .custom-dialog-container {
    position: relative;
    margin: 0 auto 50px;
    background: #fff;
    border-radius: 2px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
    box-sizing: border-box;
     50%;
  }
}

.el-aside {
  color: #333;
  text-align: center;
  .custom-aside__wrapper {
     100%;
    position: relative;
    background-color: #1f2d3d;
    .custom-aside__title {
      position: absolute;
      top: 0;
      left: 0;
       200px;
      height: 60px;
      line-height: 60px;
      font-size: 18px;
      color: #fff;
      background: #212121;
      font-weight: bolder;
    }
    .upload-demo {
      position: absolute;
      top: 80px;
      left: 0;
      .el-upload-dragger {
         180px;
      }
    }
  }
}

.el-header {
  color: #333;
  box-shadow: 0 6px 6px 0 rgba(0, 0, 0, .16);
  .custom-dialog__header {
    padding: 20px 20px 10px 20px;
    .custom-dialog__title {
      line-height: 24px;
      font-size: 18px;
      color: #303133;
    }
    .custom-dialog__headerbtn {
      position: absolute;
      top: 20px;
      right: 20px;
      padding: 0;
      background: transparent;
      border: none;
      outline: none;
      cursor: pointer;
      font-size: 16px;
    }
  }
}

.el-main {
  background-color: #E9EEF3;
  color: #333;
  text-align: center;
  height: 60vh;
  overflow: scroll;
  ul {
    li {
      float: left;
       24%;
      margin-top: 1.5vh;
      .item-bg-images {
         100%;
        height: 100%;
        background-repeat: no-repeat;
        background-size: 100% 100%;
        .item-active {
           100%;
          height: 100%;
          display: flex;
          justify-content: center;
          align-items: center;
          background-color: rgb(0, 0, 0);
          opacity: 0.7;
          span {
            color: #31b4a7;
            font-size: 52px;
          }
        }
      }
    }
    li:nth-child(4n + 1) {
      margin-left: 0px;
    }
    li:nth-child(4n + 2) {
      margin-left: 1%;
    }
    li:nth-child(4n + 3) {
      margin-left: 1%;
    }
    li:nth-child(4n + 4) {
      margin-left: 1%;
    }
  }
}

.el-footer {
  text-align: center;
  line-height: 60px;
}
</style>
//srcpageseditorcomponentsTheTemplateList.vue
<template>
  <!--region 左边在线模板列表-->
  <div class="the-template-list">

  </div>
  <!--endregion-->
</template>

<script>
export default {
  components: {},
  data () {
    return {}
  },
  created () {
  },
  mounted () {
  },
  computed: {},
  methods: {}
}
</script>

<style lang="scss">
@import "../../../styles/variables";
.the-template-list {
  background-color: #fff;
  box-sizing: border-box;
  border-right: 1px solid $border-color-1;
  flex: 0 0 $template-width;
}
</style>

//srcvuexconstTypes.js
export default {
  // 页面组件
  ADD_COMPONENT: 'add_component', // 添加组件
  TOGGLE_COMP: 'toggle_component', // 切换组件
  COPY_COMP: 'copy_comp', // 拷贝组件
  EDIT_COMP: 'edit_component', // 编辑组件
  REMOVE_COMP: 'remove_component', // 删除组件
  // 页面
  ADD_PAGE: 'add_page', // 添加新页面
  INSERT_PAGE: 'insert_page', // 插入页面
  COPY_PAGE: 'copy_page', // 拷贝页面
  REMOVE_PAGE: 'remove_page', // 删除页面
  EDIT_PAGE: 'edit_page', // 编辑页面
  TOGGLE_PAGE: 'toggle_page', // 切换页面
  ADD_COMP_TO_PAGES: 'add_component_to_page', // 往页面中添加组件
  REMOVE_COMP_TO_PAGES: 'remove_component_to_page', // 往页面中删除组件
  // 属性编辑调整
  OPEN_PROPS_PANEL: 'open_props_panel', // 打开组件属性面板
  CLOSE_PROPS_PANEL: 'close_props_panel', // 关闭组件属性面板
  // 图层编辑调整
  OPEN_LAYER_PANEL: 'open_layer_panel', // 打开图层属性面板
  CLOSE_LAYER_PANEL: 'close_layer_panel' // 关闭图层属性面板
}
//srcvuexmodulespages.js
import types from '../constTypes.js'
import { getNewPageId, getNewPage } from '../function.js'
import { cloneObj } from '../../assets/utils/util.js'

const state = {
  list: [],
  curPageId: null
}
const getters = {
  // 取页面列表
  pages: (state) => state.list,
  // 取当前页面ID
  curPageId: (state) => {
    if (state.curPageId) {
      return state.curPageId
    }
    if (state.list[0]) {
      return state.list[0]['id']
    }
  },
  // 取当前页面
  curPage: (state) => {
    return state.list
      .find(
        (_x) => _x.id === state.curPageId
      ) || state.list[0]
  },
  getPageConfigByPageId: (state) => (pageId) => {
    return state.list.find(_x => _x.id === pageId)
  }
}
const mutations = {
  // 切换页面
  [types.TOGGLE_PAGE] (state, pageId) {
    state.curPageId = pageId
  },
  // 添加页面
  [types.ADD_PAGE] (state, pageData) {
    state.list.push(pageData)
  },
  // 插入页面
  [types.INSERT_PAGE] (state, { page, pageId }) {
    let index = state.list
      .findIndex(
        _x => _x.id === pageId
      )
    if (index > -1) {
      state.list.splice(index + 1, 0, page)
    }
  },
  // 拷贝页面
  [types.COPY_PAGE] (state, { prePageId, pageId }) {
    let list = state.list
    let index = list.findIndex(_x => _x.id === prePageId)
    if (index > -1) {
      // 复制一个新的页面JSON对象
      let pageData = cloneObj(list[index])
      pageData.id = pageId
      // 克隆的页面中组件处理
      // if (pageData.comps && pageData.comps.length > 0) {
      //   for (let i = 0; i < pageData.comps.length; i++) {
      //     pageData.comps[i].id = pageData.comps[i].id + (i + 1) * pageData.comps[i].id
      //     pageData.comps[i].parentId = pageId
      //   }
      // }
      list.splice(index + 1, 0, pageData)
      state.curPageId = pageId
    }
  },
  // 删除页面
  [types.REMOVE_PAGE] (state, pageId) {
    let list = state.list
    let index = list.findIndex(_x => _x.id === pageId)
    if (index > -1) {
      list.splice(index, 1)
      let nextActivePage = list[index] || list[index - 1]
      state.curPageId = nextActivePage['id']
    }
  },
  // 编辑当前页面
  [types.EDIT_PAGE] (state, { type, value, pageId }) {
    let page = state.list
      .find(
        (_x) => _x.id === pageId || _x.id === state.curPageId
      )
    if (page) {
      let pageProp = page[type]
      for (let key in value) {
        if (!pageProp[key]) {
          continue
        }
        if (typeof value[key] === 'object') {
          Object.assign(pageProp[key], value[key])
        } else {
          pageProp[key] = value[key]
        }
      }
    }
  },
  // 往页面中添加组件
  [types.ADD_COMP_TO_PAGES] (state, compData) {
    let curPage = state.list
      .find((_x) => _x.id === state.curPageId)
    if (curPage) {
      curPage.comps.push(compData)
    }
  },
  // 往页面中删除组件
  [types.REMOVE_COMP_TO_PAGES] (state, compId) {
    let curPage = state.list.find((_x) => _x.id === state.curPageId)
    if (curPage) {
      let comps = curPage.comps
      let index = curPage.comps.findIndex(_x => _x.id === compId)
      if (index > -1) {
        comps.splice(index, 1)
        if (comps && comps.length > 0) {
          curPage.comps = comps
        } else {
          curPage.comps = []
        }
      }
    }
  }
}
const actions = {
  // 添加新的页面
  addNewPages ({ commit }) {
    const page = getNewPage()
    if (page) {
      commit(types.ADD_PAGE, page)
    }
    return page.id
  },
  // 拷贝指定页面
  copyPage ({ commit }, pageId) {
    let id = getNewPageId()
    if (id) {
      commit(types.COPY_PAGE, {
        prePageId: pageId,
        pageId: id
      })
    }
    return id
  },
  // 删除指定页面
  removePage ({ commit }, pageId) {
    commit(types.REMOVE_PAGE, pageId)
    return pageId
  },
  // 从页面中删除组件
  removeComponentToPage ({ commit }, compId) {
    commit(types.REMOVE_COMP_TO_PAGES, compId)
    return compId
  },
  /**
   * 初始化Editor 编辑器
   * 同时新建一个页面
   **/
  initPageEditor ({ dispatch, commit }) {
    dispatch('addNewPages')
      .then((id) => {
        commit(types.TOGGLE_PAGE, id)
      })
  },
  /**
   * 切换页面
   * @param commit
   * @param pageId
   */
  togglePage ({ commit }, pageId) {
    commit(types.TOGGLE_PAGE, pageId)
  },
  /**
   * 修改当前页面
   * @param commit
   * @param type
   * @param value
   * @param pageId
   */
  editCurPage ({ commit }, { type, value, pageId }) {
    commit([types.EDIT_PAGE], { type, value, pageId })
  },
  /**
   * 更新页面中的某个组件中的css属性
   * @param commit
   * @param getters
   * @param state
   * @param type
   * @param key
   */
  editCompOfCurPage ({ commit, getters, state }, { type, key }) {
    // 首先找到当前页面
    let _page = state.list.find(_x => _x.id === state.curPageId)
    if (_page) {
      // 找当前页面中的组件
      let _comp = _page.comps.find(_x => _x.id === window.$globalHub.$store.state.components.curCompId)
      let _obj = _comp[type]
      if (typeof _obj === 'object') {
        if (typeof _obj[key] === 'object') {
          Object.assign(_obj[key], window.$globalHub.$store.getters.curComp[type][key])
          console.log(' state.list:', state.list)
        } else {
          _obj[key] = window.$globalHub.$store.getters.curComp[type][key]
          console.log(' state.list:', state.list)
        }
      }
    }
  }
}
export default {
  state,
  getters,
  actions,
  mutations
}

这个里面主要是数据的流向,真的简化了很多dom的思想不需要dom思想

//srcpageseditorcomponentsTheTemplateList.vue
<template>
  <!--region 页面列表-->
  <div class="the-page-list">
    <div class="title">页面管理</div>
    <div class="action-group">
      <div class="item" @click="handleAddPage">
        <span class="axon-icon" v-html="'&#xe635;'"></span>添加
      </div>
      <div class="item" @click="handleCopyPage">
        <span class="axon-icon" v-html="'&#xe62f;'"></span>复制
      </div>
      <div class="item" @click="handleRemovePage">
        <span class="axon-icon" v-html="'&#xe63d;'"></span>删除
      </div>
    </div>
    <ul class="list">
      <template v-for="(page, key) in pages">
        <li :key="key"
            :id="page.id"
            :class="curPageId === page.id ? 'page active' : 'page'" @click="handleTogglePage(page.id)">
          <span class="key">
            <em>{{ key + 1 }}</em>
          </span>
          <p class="name">第 {{ key + 1 }} 页</p>
          <span class="more-action axon-icon" v-html="'&#xe61c;'">
          </span>
        </li>
      </template>
    </ul>
  </div>
  <!--endregion-->
</template>

<script>
export default {
  components: {},
  data () {
    return {}
  },
  created () {
  },
  mounted () {
  },
  computed: {
    // 取页面列表
    pages () {
      try {
        return this.$store.getters.pages
      } catch (e) {
        this.$notify.error({
          title: '错误',
          message: '初始化页面失败,请刷新后重试!'
        })
      }
    },
    // 当前页面ID
    curPageId () {
      try {
        return this.$store.getters.curPageId
      } catch (e) {
        this.$notify.error({
          title: '错误',
          message: '初始化页面失败,请刷新后重试!'
        })
      }
    }
  },
  methods: {
    /**
     * 创建新页面
     */
    handleAddPage () {
      this.$store.dispatch('addNewPages', this.curPageId).then((id) => {
        this.handleTogglePage(id)
      })
    },
    /**
     * 拷贝当前页面
     */
    handleCopyPage () {
      this.$store.dispatch('copyPage', this.curPageId).then((id) => {
        this.handleTogglePage(id)
      })
    },
    /**
     * 删除当前页面
     */
    handleRemovePage () {
      this.$store.dispatch('removePage', this.curPageId)
    },
    /**
     * 切换页面
     * @param pageId
     */
    handleTogglePage (pageId) {
      this.$store.dispatch('togglePage', pageId)
    }
  }
}
</script>

<style lang="scss">
@import "../../../styles/variables";

.the-page-list {
  background-color: #fafafa;
  border-left: 1px solid $border-color-1;
  box-sizing: border-box;
  flex: 0 0 $setting-width;
  // 页头
  .title {
    height: 40px;
    line-height: 40px;
    text-align: center;
    background-color: #e0e0e0;
    font-weight: 600;
  }
  // 列表
  .list {
    .page {
      border-top: 1px solid #e6ebed;
      height: 70px;
      line-height: 70px;
      color: #666;
      cursor: pointer;
      font-size: 12px;
      position: relative;
      border-bottom: 1px solid #e6ebed;
      display: flex;
      .key, .more-action {
        flex: 0 0 44px;
        text-align: center;
        display: block;
        em {
          display: inline-block;
           24px;
          height: 24px;
          line-height: 24px;
          text-align: center;
          border-radius: 12px;
          background-color: #ccc;
          color: #fff;
          font-weight: 400;
          font-style: normal;
        }
      }
      .more-action {
        font-size: 13px;
      }
      .name {
        font-size: 14px;
        flex: 100%;
      }
      &.active {
        background-color: #eee;
        .key {
          em {
            background-color: #1593ff;
          }
        }
        .name {
          color: #000;
        }
      }
    }
  }
  .action-group {
    display: flex;
    height: 36px;
    line-height: 36px;
    .item {
      flex: 0 0 33.33%;
      text-align: center;
      font-size: 12px;
      &:not(:last-child) {
        border-right: 1px solid #eee;
      }
      span {
        font-size: 14px;
        padding-right: 4px;
      }
    }
  }
}
</style>
//srcpageseditorcomponentsTheEditorContainer.vue
<template>
  <div class="the-editor-container">
    <div class="container-wrapper">
      <!--region 页面列表-->
      <div class="page-list">
        <div class="page-wrapper">
          <!--region 背景-->
          <div class="wrapper-background"
               :style="{
                   backgroundColor: curPage.css.bgc,
                   backgroundImage: 'url(' + curPage.css.bgi + ')'
                 }"></div>
          <!--endregion-->
          <!--region 组件-->
          <div class="component-wrapper">
            <template v-for="comp in curPage.comps">
              <vue-drr
                :ref="'comp_' + comp.id"
                :id="'comp_' + comp.id"
                :key="comp.id"
                :w="parseInt(comp.css.w)"
                :h="parseInt(comp.css.h)"
                :x="parseInt(comp.css.l)"
                :y="parseInt(comp.css.t)"
                :minw="20"
                :minh="30"
                :grid="grid"
                :parent="true"
                :angle="parseInt(comp.css.rotate)"
                @activated="toggleComp(comp.id, comp.name)"
                @resizestop="handleResizing"
                @dragstop="handleDragging"
                @rotatestop="handleRotating">
                <comp-list
                  @click="handleClick(comp)"
                  @dblclick="handleDbClick"
                  @contextmenu="handleContextmenu(comp)"
                  :id="comp.id"
                  class="comp"
                  :style="comp.css | formatStyle('ft', 'lh')"
                  :type="comp.name"></comp-list>
              </vue-drr>
            </template>
          </div>
          <!--endregion-->
        </div>
      </div>
      <!--endregion-->
    </div>
  </div>
</template>

<script>
import vueDrr from 'vue-drr'
import { throttle } from '../../../assets/utils/util.js'
import BaseComps from '../../../components/iTemplate/index.js'

const BASE_COMP_NAME = 'Base'
const BASE_COMP_CONFIG = 'Config'
export default {
  components: {
    vueDrr,
    compList: {
      props: {
        id: {
          type: [String, Number],
          required: true
        }, // 组件ID
        type: {
          type: String,
          required: true
        } // 组件类型名称
      },
      render (h) {
        let _compFlagName = `${BASE_COMP_NAME}${this.type}`
        let _module = BaseComps[_compFlagName]
        return h(_module, {
          props: {
            id: this.id
          },
          nativeOn: {
            click: throttle(this.handleClick, 1000),
            dblclick: this.handleDbClick,
            // contextmenu: throttle(this.handleContextmenu, 1000)
            contextmenu: evt => {
              // 阻止浏览器默认点击行为
              evt.preventDefault()
              throttle(this.handleContextmenu(this.comp), 1000)
            }
          }
        })
      },
      methods: {
        // 单击:左击
        handleClick (comp) {
          this.$emit('click', comp)
        },
        // 双击
        handleDbClick () {
          this.$emit('dblclick', this.type)
        },
        // 单击:右击
        handleContextmenu (comp) {
          this.$emit('contextmenu', comp)
        }
      }
    }
  },
  data () {
    return {
      grid: [1, 1] // 组件拖动的网格
    }
  },
  created () {
  },
  mounted () {
  },
  computed: {
    // 取当前页面
    curPage () {
      try {
        return this.$store.getters.curPage
      } catch (e) {
        this.$notify.error({
          title: '错误',
          message: '初始化页面失败,请刷新后重试!'
        })
      }
    },
    // 取当前正在编辑的组件
    curCompId () {
      return this.$store.state.components.curCompId
    },
    // 取当前组件
    curComp () {
      return this.$store.getters.currComp
    }
  },
  methods: {
    // 切换组件
    toggleComp (id, name) {
      // 打开组件属性设置面板
      this.$store.dispatch('openPropsPanel', {
        id: id,
        name: name + BASE_COMP_CONFIG
      })
      this.$store.dispatch('toggleComp', id)
    },
    // 左击:点击组件
    handleClick (comp) {
      // 关闭图层面板
      this.$store.dispatch('closeLayerPanel')
      // 打开组件属性设置面板
      this.$store.dispatch('openPropsPanel', {
        id: comp.id,
        name: comp.name + BASE_COMP_CONFIG
      })
    },
    /**
     * 双击组件
     * @param type 组件类型
     */
    handleDbClick (type) {
      console.log('双击666')
    },
    handleContextmenu (comp) {
      console.log('右击666')
      this.$store.dispatch('closePropsPanel')
      // 打开图层设置面板
      this.$store.dispatch('openLayerPanel', {
        id: comp.id,
        name: comp.name + 'layer' + BASE_COMP_CONFIG
      })
    },
    // 组件拉伸时触发
    handleResizing (x, y, w, h) {
      this.updateCompStyle({ l: x, t: y, w, h })
    },
    // 组件拖动时触发
    handleDragging (x, y) {
      this.updateCompStyle({ l: x, t: y })
    },
    // 组件旋转时触发
    handleRotating (angle) {
      this.updateCompStyle({ rotate: angle })
    },
    // 同步用户修改
    updateCompStyle (value) {
      this.$store.dispatch('editComp', {
        type: 'css',
        value: value
      })
    }
  }
}
</script>

<style lang="scss">
.the-editor-container {
  margin: 0 auto;
  border: 2px solid #ddd;
  border-radius: 2px;
   52.7rem;
  min-height: 82.6rem;
  flex: 0 0 1;
  height: 100%;
  background-color: #fff;
  position: relative;
  .container-wrapper {
     100%;
    height: 100%;
    .page-list {
       100%;
      height: 100%;
      .page-wrapper {
         100%;
        height: 100%;
        .wrapper-background {
          position: absolute;
          top: 0;
          left: 0;
           100%;
          height: 100%;
          background-repeat: no-repeat;
          background-size: cover;
          background-origin: content-box;
          background-position: 50% 50%;
        }
        .component-wrapper {
           100%;
          height: 100%;
          position: relative;
        }
      }
    }
  }
}
</style>
//srcpageseditorcomponentsTheCompPropsConfig.vue
<!--region 组件属性设置面板-->
<template>
  <div ref="propsPanelConfig" class="the-comp-props-config-page" v-if="propsPanel && propsPanel.show"
       :style="{ height: panelHeight }">
    <div :class="isMouseDown ? 'header pointer-events': 'header'">
      <div class="title" ref="header" @mousedown="handleDragMouseDown">组件设置</div>
      <div class="close">
        <span class="axon-icon" v-html="'&#xe622;'" @click="closePropsPanel()"></span>
      </div>
    </div>
    <div class="props-panel-config">
      <props-panel :name="propsPanel.name"></props-panel>
    </div>
  </div>
</template>
<!--endregion-->
<script>
import CompPropsConfig from '../../../components/iTemplate/config.js'
import { throttle } from '../../../assets/utils/util.js'

export default {
  components: {
    propsPanel: {
      props: {
        name: {
          type: String,
          required: true
        }
      },
      render (h) {
        let _module = CompPropsConfig[this.name]
        return h(_module, {
          props: {
            name: {
              type: String
            }
          }
        })
      }
    }
  },
  data () {
    return {
      isMouseDown: false, // 鼠标是否按住可拖放区域
      dragObj: {
        initX: 0,
        indexY: 0,
         260,
        height: 48
      } // 拖动对象位置
    }
  },
  created () {
  },
  mounted () {
    document.addEventListener('mousemove', (e) => { this.throttleMove(e) })
    document.addEventListener('mouseup', (e) => { this.handleDragMouseUp(e) })
  },
  computed: {
    // 根据父级组件高度设定属性面板高度
    panelHeight () {
      // 取父级组件中的父级标签属性
      let _parentHeight = 0
      try {
        _parentHeight = this.$parent.$refs.centerBox.offsetHeight
        return `${_parentHeight - 100}px`
      } catch (e) {
        return 0
      }
    },
    // 取面板信息
    propsPanel () {
      return this.$store.getters.propPanel
    }
  },
  watch: {},
  methods: {
    // 关闭属性面板
    closePropsPanel () {
      console.log(' close:')
      this.$store.dispatch('closePropsPanel')
    },
    // 拖动:当鼠标点下的时候,给要拖动的元素附上初始值
    handleDragMouseDown (e) {
      this.isMouseDown = true
      this.dragObj.initX = e.offsetX
      this.dragObj.initY = e.offsetY
      this.dragObj.width = this.$refs.propsPanelConfig.offsetWidth
      this.dragObj.height = this.$refs.propsPanelConfig.offsetHeight - this.$refs.header.offsetHeight
    },
    // 鼠标移动 --- 节流
    throttleMove (e) {
      return throttle(this.handleDragMouseMove(e), 500)
    },
    // 拖动过程中,需要实时监听位置变化
    handleDragMouseMove (e) {
      if (this.isMouseDown) {
        // 移动外层需要移动的div
        let _cx = e.clientX - this.dragObj.initX
        let _cy = e.clientY - this.dragObj.indexY
        // 限制panel不能超出浏览器边界
        _cx = _cx >= 0 ? _cx : 0
        _cy = _cy >= 0 ? _cy : 0
        if (window.innerWidth - e.clientX + this.dragObj.initX < this.dragObj.width + 16 || window.innerWidth - this.dragObj.width < _cx) {
          _cx = window.innerWidth - this.dragObj.width
        }
        if (e.clientY > window.innerHeight - this.dragObj.height - this.$refs.header.offsetHeight + this.dragObj.initY) {
          _cy = window.innerHeight - this.$refs.header.offsetHeight - this.dragObj.height
        }
        this.$refs.propsPanelConfig.style.left = _cx + 'px'
        this.$refs.propsPanelConfig.style.top = _cy + 'px'
      }
    },
    // 鼠标移除,取消监听
    handleDragMouseUp (e) {
      if (e.clientY > window.innerWidth || e.clientY < 0 || e.clientX < 0 || e.clientX > window.innerHeight) {
        this.isMouseDown = false
      }
      this.isMouseDown = false
    }
  }
}
</script>

<style lang="scss">
.the-comp-props-config-page {
  background-color: #fafafa;
  position: fixed;
  left: 0;
  top: 0;
   260px;
  overflow: hidden;
  box-shadow: 0 0 16px rgba(0, 0, 0, 0.16);
  z-index: 1000;
  height: 100%;
  user-select: none;
  .header {
    background-color: #fff;
    height: 48px;
    line-height: 48px;
     100%;
    padding: 0 16px;
    user-select: none;
    display: flex;
    &.pointer-events {
      pointer-events: none;
    }
    .title {
      flex: 0 0 50%;
      font-size: 13px;
      cursor: move;
    }
    .close {
      flex: 0 0 50%;
      text-align: right;
      z-index: 9999;
      span {
        cursor: pointer;
        font-size: 14px;
        color: #a3afb7;
      }
    }
  }
  .props-panel-config {
    height: calc(#{"100% - 54px"});
    .text-comp-config-page {
      height: 100%;
      .el-tabs {
        height: 100%;
        overflow: hidden;
        .el-tabs__content {
          height: calc(#{"100% - 50px"});
          overflow-y: auto;
          .el-tab-pane {
            height: 100%;
          }
        }
      }
    }
  }
}
</style>
//srcpageseditorcomponentsTheCompLayerManager.vue
<!--region 图层设置面板-->
<template>
  <div ref="layerManagerPanel" class="the-comp-layer-manager-page" v-if="layerPanel && layerPanel.show">
    <div :class="isMouseDown ? 'header pointer-events': 'header'">
      <div class="title" ref="header" @mousedown="handleDragMouseDown">图层设置</div>
      <div class="close">
        <span class="axon-icon" v-html="'&#xe622;'" @click="closeLareyPanel()"></span>
      </div>
    </div>
    <div class="layer-panel-list">
      <ul>
        <li @click="handRemove">删除组件</li>
        <li @click="handleCopyComp">拷贝组件</li>
        <!---图层显示依据数组的降序排练--关于图层数据的操作要反过来-->
        <li @click="handleSwapItem('down')">上移一层</li>
        <li @click="handleSwapItem('up')">下移一层</li>
        <li @click="handleSwapItem('bottom')">置顶</li>
        <li @click="handleSwapItem('top')">置底</li>
      </ul>
    </div>
  </div>
</template>
<!--endregion-->
<script>
import { throttle } from '../../../assets/utils/util.js'
import SwapArrayItem from '../../../assets/utils/swapArrayItem.js'

export default {
  components: {},
  data () {
    return {
      isMouseDown: false, // 鼠标是否按住可拖放区域
      dragObj: {
        initX: 0,
        indexY: 0,
         160,
        height: 48
      } // 拖动对象位置
    }
  },
  created () {
  },
  mounted () {
    document.addEventListener('mousemove', (e) => { this.throttleMove(e) })
    document.addEventListener('mouseup', (e) => { this.handleDragMouseUp(e) })
  },
  computed: {
    // 取图层信息
    layerPanel () {
      return this.$store.getters.layerPanel
    },
    // 取当前页面ID
    curPageId () {
      return this.$store.getters.curPage.id
    },
    // 取当前页面中所有组件
    curPageComps () {
      return this.$store.getters.curPage.comps
    },
    // 取当前正在编辑的组件
    curCompId () {
      return this.$store.state.components.curCompId
    }
  },
  watch: {},
  methods: {
    // 关闭图层面板
    closeLareyPanel () {
      this.$store.dispatch('closeLayerPanel')
    },
    // 删除当前组件
    handRemove () {
      let compId = this.curCompId
      // 删除当前组件
      this.$store.dispatch('removeComp', compId)
      // 将组件从当前页面中移除
      this.$store.dispatch('removeComponentToPage', compId)
      // 关闭图层面板
      this.$store.dispatch('closeLayerPanel')
    },
    // 拷贝当前组件
    handleCopyComp () {
      this.$store.dispatch('copyComp', this.curCompId)
    },
    // 交换数组元素
    handleSwapItem (operateName) {
      // 获取当前数组下标
      let index = this.accpetIndex(this.curPageComps, this.curCompId)
      SwapArrayItem.swapByOperate(this.curPageComps, index, operateName)
    },
    /**
     * 获取下标
     * @param ary
     * @param id
     * @returns {number}
     */
    accpetIndex (ary, id) {
      var index = -1
      for (let i = 0; i < ary.length; i++) {
        if (ary[i] && ary[i].id === id) {
          index = i
        }
      }
      return index
    },
    // 拖动:当鼠标点下的时候,给要拖动的元素附上初始值
    handleDragMouseDown (e) {
      this.isMouseDown = true
      this.dragObj.initX = e.offsetX
      this.dragObj.initY = e.offsetY
      this.dragObj.width = this.$refs.layerManagerPanel.offsetWidth
      this.dragObj.height = this.$refs.layerManagerPanel.offsetHeight - this.$refs.header.offsetHeight
    },
    // 鼠标移动 --- 节流
    throttleMove (e) {
      return throttle(this.handleDragMouseMove(e), 500)
    },
    // 拖动过程中,需要实时监听位置变化
    handleDragMouseMove (e) {
      if (this.isMouseDown) {
        // 移动外层需要移动的div
        let _cx = e.clientX - this.dragObj.initX
        let _cy = e.clientY - this.dragObj.indexY
        // 限制panel不能超出浏览器边界
        _cx = _cx >= 0 ? _cx : 0
        _cy = _cy >= 0 ? _cy : 0
        if (window.innerWidth - e.clientX + this.dragObj.initX < this.dragObj.width + 16 || window.innerWidth - this.dragObj.width < _cx) {
          _cx = window.innerWidth - this.dragObj.width
        }
        if (e.clientY > window.innerHeight - this.dragObj.height - this.$refs.header.offsetHeight + this.dragObj.initY) {
          _cy = window.innerHeight - this.$refs.header.offsetHeight - this.dragObj.height
        }
        this.$refs.layerManagerPanel.style.left = _cx + 'px'
        this.$refs.layerManagerPanel.style.top = _cy + 'px'
      }
    },
    // 鼠标移除,取消监听
    handleDragMouseUp (e) {
      if (e.clientY > window.innerWidth || e.clientY < 0 || e.clientX < 0 || e.clientX > window.innerHeight) {
        this.isMouseDown = false
      }
      this.isMouseDown = false
    }
  }
}
</script>

<style lang="scss">
.the-comp-layer-manager-page {
  background-color: #fafafa;
  position: fixed;
  left: 0;
  top: 0;
   160px;
  overflow: hidden;
  box-shadow: 0 0 16px rgba(0, 0, 0, 0.16);
  z-index: 1000;
  height: 286px;
  user-select: none;
  box-sizing: content-box;
  .header {
    background-color: #fff;
    height: 48px;
    line-height: 48px;
     100%;
    padding: 0 16px;
    user-select: none;
    display: flex;
    &.pointer-events {
      pointer-events: none;
    }
    .title {
      flex: 0 0 50%;
      font-size: 13px;
      cursor: move;
    }
    .close {
      flex: 0 0 30%;
      text-align: right;
      z-index: 9999;
      span {
        cursor: pointer;
        font-size: 14px;
        color: #a3afb7;
      }
    }
  }
  .layer-panel-list {
    height: calc(#{"100% - 54px"});
    font-size: 13px;
    li {
      height: 40px;
      line-height: 40px;
      padding: 0 16px;
      box-sizing: content-box;
      border-top: 1px solid #e6ebed;
      &:hover {
        color: #ffffff;
        background-color: #1593ff;
      }
    }
    li:nth-child(1) {
      &:hover {
        color: #ffffff;
        background-color: #ff2a6a;
      }
    }
  }
}
</style>

//srcpagesiconsIndex.vue
<template>
  <div class="icon-page">
    <el-row :gutter="20">
      <template v-for="(item, index) in icons">
        <el-col :key="index" :span="6" class="item">
          <span class="axon-icon" v-html="item.icon"></span>
          <p><span style="color: #606266;height: 1em;font-size: 12px;">{{item.icon}}</span></p>
        </el-col>
      </template>
    </el-row>
  </div>
</template>
<script>
export default {
  components: {},
  data () {
    return {
      icons: [{ name: '叉号', icon: '&#xe603;' },
        { name: '原型叉号', icon: '&#xe614;' },
        { name: '栏目', icon: '&#xe61f;' },
        { name: '箭头', icon: '&#xe608;' },
        { name: '隐藏密码', icon: '&#xe620;' },
        { name: '显示密码', icon: '&#xe60c;' },
        { name: '用户', icon: '&#xe604;' },
        { name: '用户', icon: '&#xe7e9;' },
        { name: '发货', icon: '&#xe63e;' },
        { name: '订单', icon: '&#xe72d;' },
        { name: '用户', icon: '&#xe6a3;' },
        { name: '金额', icon: '&#xe736;' },
        { name: '主页', icon: '&#xe63d;' },
        { name: '错误', icon: '&#xe625;' },
        { name: '错误', icon: '&#xe610;' },
        { name: '栏目', icon: '&#xe626;' },
        { name: '栏目', icon: '&#xe60a;' },
        { name: '验证码', icon: '&#xe61b;' },
        { name: '手机', icon: '&#xe60d;' }
      ]
    }
  },
  mounted () {},
  methods: {}
}
</script>

<style lang="less" rel="stylesheet/less" scoped>
.icon-page {
  background-color: #fff;
  padding: 20px 40px;
  .item {
    text-align: center;
    height: 120px;
    .axon-icon {
      font-size: 24px;
      color: #606266;
      margin-bottom: 15px;
      display: block;
    }
  }
}
</style>

//srcpages	ableIndex.vue

<template>
  <div class="table-page">
    <!--region table 表格-->
    <i-table :list="list" :total="total" :loading="loading" :otherHeight="otherHeight"
             @handleSizeChange="handleSizeChange"
             @handleIndexChange="handleIndexChange" @handleSelectionChange="handleSelectionChange" :options="options"
             :pagination="pagination" :columns="columns" :operates="operates">
    </i-table>
    <!--endregion-->
  </div>
</template>
<script type="text/jsx">
import iTable from '../../components/iTable/Index'
import BLL from './Index'
import { mapGetters } from 'vuex'

export default {
  components: { iTable },
  data () {
    return {
      total: 0,
      list: [],
      otherHeight: 208,
      filter: {
        date: '',
        phone: ''
      },
      collapseTitle: '筛选条件:',
      columns: [
        {
          prop: 'id',
          label: '编号',
          align: 'center',
           60
        },
        {
          prop: 'title',
          label: '标题',
          align: 'center',
           400,
          formatter: (row, column, cellValue) => {
            return `<span style="white-space: nowrap;color: dodgerblue;">${row.title}</span>`
          }
        },
        {
          prop: 'state',
          label: '状态',
          align: 'center',
           '160',
          render: (row, column) => {
            return row.state === 0 ? <el-tag type='success'> 上架 </el-tag> : row.state === 1
              ? <el-tag type="info">下架</el-tag> : <el-tag type='danger'>审核中</el-tag>
          }
        }, {
          prop: 'author',
          label: '作者',
          align: 'center',
           120
        },
        {
          prop: 'phone',
          label: '联系方式',
          align: 'center',
           160
        },
        {
          prop: 'email',
          label: '邮箱',
          align: 'center',
           240
        },
        {
          prop: 'createDate',
          label: '发布时间',
          align: 'center'
        }
      ], // 需要展示的列
      operates: {
         200,
        fixed: 'right',
        list: [
          {
            label: '编辑',
            type: 'warning',
            show: true,
            icon: 'el-icon-edit',
            plain: false,
            disabled: false,
            method: (index, row) => {
              this.handleEdit(index, row)
            }
          },
          {
            label: '删除',
            type: 'danger',
            icon: 'el-icon-delete',
            show: true,
            plain: false,
            disabled: false,
            method: (index, row) => {
              this.handleDel(index, row)
            }
          }
        ]
      }, // 操作按钮组
      pagination: {
        pageIndex: 1,
        pageSize: 20
      }, // 分页参数
      options: {
        stripe: true, // 是否为斑马纹 table
        highlightCurrentRow: true, // 是否支持当前行高亮显示
        mutiSelect: true // 是否支持列表项选中功能
      } // table 的参数
    }
  },
  created () {
    this.BLL = new BLL(this)
  },
  mounted () {
    this.BLL.getList()
  },
  computed: {
    ...mapGetters(['button']),
    loading () {
      return this.$store.getters.btnLoading.str && this.$store.getters.btnLoading.id
    }
  },
  methods: {
    // 切换每页显示的数量
    handleSizeChange (pagination) {
      this.pagination = pagination
      this.BLL.getList()
    },
    // 切换页码
    handleIndexChange (pagination) {
      this.pagination = pagination
      this.BLL.getList()
    },
    // 选中行
    handleSelectionChange (val) {
      console.log('val:', val)
    },
    // 编辑
    handleEdit (index, row) {
      console.log(' index:', index)
      console.log(' row:', row)
      this.BLL.getList()
    },
    // 删除
    handleDel (index, row) {
      console.log(' index:', index)
      console.log(' row:', row)
    },
    // 刷新
    reload (form) {
      console.log('into reload')
      this.$refs['filter'].resetFields()
      this.filter = { date: '', phone: '' }
      this.BLL.getList()
    },
    // 筛选数据
    filterData () {
      this.BLL.getList()
    }
  }
}
</script>

<style lang="scss">
@import "./Index";
</style>

//srcpagesmenusIndex.vue
<!--菜单页-->
<template>
  <div class="menus-page">
    <div slot="header" class="clearfix header">
    </div>
    <i-table :list="menusList" :otherHeight="otherHeight" :columns="columns" :operates="operates"></i-table>
  </div>
</template>

<script type="text/jsx">
import iTable from '../../components/iTable/Index'
import { mapState } from 'vuex'

export default {
  components: { iTable },
  data () {
    return {
      menusList: [],
      columns: [
        {
          prop: 'name',
          label: '名称',
          align: 'left',
           200,
          className: 'first-level',
          formatter: function (row, column) {
            let marginLeft = parseInt(row.level) * 20
            if (row.icon) {
              let icon = `<span class="axon-icon" style="margin-right:4px;margin-left:${marginLeft}px;font-size:16px;">${row.icon}</span>`
              return `${icon}<span>${row.name}</span>`
            } else {
              return `<span style="margin-left:${marginLeft}px;">${row.name}</span>`
            }
          }
        }, {
          prop: 'link',
          label: '地址',
          align: 'center'
        }, {
          prop: 'level',
          label: '排序',
          align: 'center',
           100
        }, {
          prop: 'state',
          label: '状态',
          align: 'center',
           100,
          render: function (row, column) {
            return row.state === 1 ? <el-tag>启用</el-tag> : <el-tag type="info">禁用</el-tag>
          }
        }
      ],
      operates: {
         200,
        fixed: 'right',
        list: [
          {
            label: '编辑',
            type: 'warning',
            show: true,
            icon: 'el-icon-edit',
            plain: true,
            disabled: false,
            method: (index, row) => {
              this.handleEdit(index, row)
            }
          },
          {
            label: '删除',
            type: 'danger',
            show: true,
            icon: 'el-icon-delete',
            plain: true,
            disabled: false,
            method: (index, row) => {
              this.handelDel(index, row)
            }
          }
        ]
      },
      otherHeight: 216
    }
  },
  mounted () {
    this.initMenus()
  },
  computed: {
    ...mapState([
      'menus'
    ]),
    loading () {
      return this.$store.getters.btnLoading.str && this.$store.getters.btnLoading.id
    }
  },
  methods: {
    initMenus () {
      if (this.menus && this.menus.sidebar.length > 0) {
        let level = 1
        this.formatMenusList(this.menus.sidebar, level)
      }
    },
    // 递归menus的各级菜单
    formatMenusList (list, level) {
      for (let i = 0; i < list.length; i++) {
        this.menusList.push({ ...list[i], level })
        if (list[i].children) {
          level++
          this.formatMenusList(list[i].children, level)
        }
      }
    },
    // 编辑菜单
    handleEdit (index, row) {
      console.log(row)
    },
    // 删除菜单
    handelDel (index, row) {
      console.log(row)
    },
    add () {
      console.log('into add()')
    },
    // 转换 string 方法名为 Function 对象
    callback (fn) {
      fn = `this.${fn}()`
      return eval(fn) // eslint-disable-line
      // this.__callback(fn, this)
    }
  }
}
</script>

<style lang="scss">
.menus-page {
  background-color: #fff;
  .header {
    padding: 10px 10px;
  }
}
</style>

<!--角色管理页面-->
<template>
  <div class="role-page">
    <i-table></i-table>
  </div>
</template>

<script>
import iTable from '../../components/iTable/Index'
export default {
  components: { iTable },
  data () {
    return {}
  },
  mounted () { },
  computed: {},
  methods: {}
}
</script>

<style lang="scss" scoped>
.role-page {
  background-color: #fff;
}
</style>

<template>
  <div class="MyContain">

  </div>
</template>

<script>
export default {
  components: {},
  data () {
    return {}
  },
  mounted () { },
  computed: {},
  methods: {}
}
</script>

<style lang="scss" scoped>
.MyContain {
  background-color: #fff;
}
</style>

<template>
  <div class="MyContain">

  </div>
</template>

<script>
export default {
  components: {},
  data () {
    return {}
  },
  mounted () { },
  computed: {},
  methods: {}
}
</script>

<style lang="scss" scoped>
.MyContain {
  background-color: #fff;
}
</style>

原文地址:https://www.cnblogs.com/smart-girl/p/11376713.html