vue动态配置小程序页面(一)

1,207 阅读1分钟

最近花时间弄了一个动态配置小程序页面的功能

主要分配置端和渲染端(参考萤火商城后台配置小程序)

设计思路

1.先设计出页面有哪些展示类组件

2.设计组件样式

3.抽取能够动态变化的配置参数

4.开发页面组件把每个功能组件分类编写样式独立文件 方便后期维护(目前我这里是写在一起的)

5.小程序端 相应的写出对应的组件参数配置模式防止组件不能渲染

icon 使用的是阿里矢量图

https://www.iconfont.cn/

一、左边组件

1.整理组件列表

2.配置点击组件默认数据

<template>
  <div class="my-menu">
    <div class="menu-title">
      <span>组件库</span>
    </div>
    <div class="navs">
      <div class="navs-group">
        <template v-for="g in navs">
          <div class="title" :key="g.type">
            {{g.title}}
          </div>
          <div class="navs-components am-cf" :key="g.title">
            <div class="special" @click="addcomponent(item)" v-for="item in g.item" :key="item.type">
              <p class="item-icon">
                <i class="iconfont" :class="[item.icon]" style="font-size: 1.8rem;"></i>
              </p>
              <p>
                {{item.name}}
              </p>
            </div>
          </div>
        </template>
      </div>
    </div>
    <div class="action">
      <el-button type="primary" @click="save">保存页面</el-button>
    </div>
  </div>
</template>
<script>
export default {
  name: '',
  data() {
    return {
      navs: [
        {
          title: '媒体组件',
          type: 'mt',
          item: [
            {
              type: 'banner',
              icon: 'icontupianlunbo',
              name: '图片轮播',
            },
            {
              type: 'imageSingle',
              icon: 'icontupian',
              name: '单组图片',
            },
            {
              type: 'video',
              icon: 'iconshipin',
              name: '视频组',
            },
            {
              type: 'article',
              icon: 'iconwenzhang',
              name: '文章组',
            },
            {
              type: 'special',
              icon: 'icontoutiao',
              name: '头条快报',
            },
          ],
        },
        {
          title: '业务组件',
          type: 'yw',
          item: [
            {
              type: 'search',
              icon: 'iconsearch',
              name: '搜索框',
            },
            {
              type: 'notice',
              icon: 'icondvt-notice',
              name: '公告组',
            },
            {
              type: 'navBar',
              icon: 'icondaohangzu',
              name: '导航组',
            },
          ],
        },
        {
          title: '工具组件',
          type: 'gj',
          item: [
            {
              type: 'service',
              icon: 'iconzaixiankefu',
              name: '在线客服',
            },
            {
              type: 'officialAccount',
              icon: 'iconguanzhugongzhonghao',
              name: '关注公众号',
            },
            {
              type: 'richText',
              icon: 'iconfuwenben',
              name: '富文本',
            },
            {
              type: 'blank',
              icon: 'iconkongbai',
              name: '辅助空白',
            },
            {
              type: 'guide',
              icon: 'iconfengexian',
              name: '辅助线',
            },
          ],
        },
      ],
      defaultimgurl: '',
      defaultimgurl1: '',
      defaultserviceurl: '',
      defaultnoticeurl: '',
    }
  },
  components: {},
  created() {},
  methods: {
    async save() {
      this.$emit('save')
    },
    addcomponent(val) {
      let item = {}
      if (val.type == 'banner') {
        item = {
          name: val.name,
          type: 'banner',
          style: {
            height: 200,
            backgroundTpye: 'all',
            background: '',
          },
          params: {
            interval: 2800,
          },
          data: [
            {
              linkUrl: '',
              imgUrl: this.defaultimgurl,
            },
            {
              linkUrl: '',
              imgUrl: this.defaultimgurl,
            },
          ],
        }
      }
      if (val.type == 'imageSingle') {
        item = {
          name: val.name,
          type: 'imageSingle',
          style: {
            paddingTop: 0,
            paddingLeft: 0,
            background: '#ffffff',
            height: 200,
          },
          data: [
            {
              linkUrl: '',
              imgUrl: this.defaultimgurl,
            },
            {
              linkUrl: '',
              imgUrl: this.defaultimgurl,
            },
          ],
        }
      }
      if (val.type == 'video') {
        item = {
          name: val.name,
          type: 'video',
          style: {
            paddingTop: 0,
            height: 200,
          },
          params: {
            videoUrl: '',
            poster: this.defaultimgurl,
            autoplay: '0',
          },
        }
      }
      if (val.type == 'navBar') {
        item = {
          name: val.name,
          type: val.type,
          style: {
            background: '',
            rowsNum: 4,
            marginLeft: 0,
            paddingTop: 0,
          },
          params: {
            title: '标题内容',
            visible: 1,
            color: '#666666',
            textAlign: 'center',
          },
          data: [
            {
              imgUrl: this.defaultimgurl,
              linkUrl: '',
              text: '按钮文字1',
              color: '#666666',
            },
            {
              imgUrl: this.defaultimgurl,
              linkUrl: '',
              text: '按钮文字2',
              color: '#666666',
            },
            {
              imgUrl: this.defaultimgurl,
              linkUrl: '',
              text: '按钮文字3',
              color: '#666666',
            },
            {
              imgUrl: this.defaultimgurl,
              linkUrl: '',
              text: '按钮文字4',
              color: '#666666',
            },
          ],
        }
      }
      if (val.type == 'article') {
        item = {
          name: '文章组',
          type: 'article',
          params: {
            source: 'auto',
            auto: {
              category: 0,
              showNum: 6,
            },
          },
          style: [],
          defaultData: [
            {
              article_title: '此处显示文章标题',
              article_cont: '这是文章的内容',
              show_type: 10,
              image: this.defaultimgurl,
              date: '2020-05-10 12:00',
            },
            {
              article_title: '此处显示文章标题',
              show_type: 10,
              article_cont: '这是文章的内容',
              image: this.defaultimgurl,
              date: '2020-05-10 12:00',
            },
          ],
          data: [],
        }
      }
      if (val.type == 'special') {
        item = {
          name: '头条快报',
          type: 'special',
          params: {
            source: 'auto',
            auto: {
              category: 0,
              showNum: 6,
            },
          },
          style: {
            display: '1',
            image: this.defaultimgurl1,
          },
          defaultData: [
            {
              article_title: '头条内容。。。',
            },
            {
              article_title: '头条内容。。。',
            },
          ],
          data: [],
        }
      }
      if (val.type == 'blank') {
        item = {
          name: '辅助空白',
          type: 'blank',
          style: {
            height: 20,
            background: '#ffffff',
          },
        }
      }
      if (val.type == 'guide') {
        item = {
          name: '辅助线',
          type: 'guide',
          style: {
            background: '#ffffff',
            lineStyle: 'solid',
            lineHeight: 1,
            lineColor: '#000000',
            paddingTop: 10,
          },
        }
      }
      if (val.type == 'service') {
        item = {
          name: '在线客服',
          type: 'service',
          params: {
            type: 'chat',
            image: this.defaultserviceurl,
            phone_num: '',
          },
          style: {
            right: 1,
            bottom: 10,
            opacity: 100,
          },
        }
      }
      if (val.type == 'notice') {
        item = {
          name: '公告组',
          type: 'notice',
          params: {
            text: '这里是第一条自定义公告的标题',
            icon: this.defaultnoticeurl,
          },
          style: {
            paddingTop: '4',
            background: '#ffffff',
            textColor: '#000000',
          },
        }
      }
      if (val.type == 'search') {
        item = {
          name: '搜索框',
          type: 'search',
          params: {
            placeholder: '请输入关键字进行搜索',
          },
          style: {
            textAlign: 'left',
            searchStyle: 'square',
          },
        }
      }
      if (val.type == 'richText') {
        item = {
          name: '富文本',
          type: 'richText',
          params: {
            content: '<p>这里是文本的内容</p>',
          },
          style: {
            paddingTop: '0',
            paddingLeft: '0',
            background: '#ffffff',
          },
        }
      }
      this.$emit('add', item)
    },
  },
}
</script>

<style lang="scss" scoped>
.my-menu {
  width: 285px;
  height: auto;
  background: #fdfdfd;
  border: 1px solid #ddd;
  padding: 15px 10px;
  transition: all 0.3s;
  user-select: none;
  .menu-title {
    position: relative;
    padding: 0 22px;
    height: 30px;
    border-bottom: 1px solid #eef1f5;
    line-height: 30px;
    &:before {
      content: '';
      position: absolute;
      width: 4px;
      height: 13px;
      background: #00aeff;
      top: 8px;
      left: 9px;
    }
  }
  .navs {
    padding: 10px 5px;
    border-bottom: 1px dotted #ddd;
    position: relative;
    display: flex;
    .navs-group {
      .title {
        // font-size: 1.24rem;
        color: #999;
        margin: 10px 0;
      }

      .navs-components {
        .special {
          width: 74px;
          float: left;
          padding: 3px 0;
          margin: 5px;
          border: 1px solid #dddddd;
          text-align: center;
          font-size: 12px;
          cursor: pointer;
          transition: All 0.3s ease-in-out;
          background: #f4f4f4;
          p {
            color: #424242;
          }
        }
      }
    }
  }
  .action {
    margin-top: 15px;
    text-align: right;
    position: relative;
  }
}
.am-cf:after {
  clear: both;
}

.am-cf:after,
.am-cf:before {
  content: ' ';
  display: table;
}
</style>

二、中间组件

1.根据默认参数渲染组件

<template>
  <div class="my-phone">
    <div class="phone-top optional" :style="{backgroundColor: pages.page.style.titleBackgroundColor}" @click="changitem(pages.page,-1)" :class="{'selected':selectitem.type=='page'}">
      <h4 :style="{color: pages.page.style.titleTextColor}">{{pages.page.params.title}}</h4>
    </div>
    <div class="phone-main">
      <div class="dragArea" :style="{background:pages.page.style.pageBackgroundColor}">
        <draggable v-model="pages.items" @start="dragstart" @end="dragend">
          <template v-for="(item,index) in pages.items">
            <div v-if="item.type!='service'" :key="index" @click="changitem(item,index)" class="drag optional" :class="{selected:selectindex==index}">
              <div v-if="item.type=='banner'" class="my-banner" :style="{paddingBottom:item.style.paddingTop+'px',backgroundColor:item.style.background}">
                <img v-for="(img,imgindex) in item.data" :key="'banner'+imgindex" :src="img.imgUrl" :style="{padding:item.style.paddingTop+'px '+item.style.paddingLeft+'px  0px',}">
              </div>
              <div v-if="item.type=='imageSingle'" class="my-imageSingle" :style="{paddingBottom:item.style.paddingTop+'px',background:item.style.background}">
                <div class="item-image" v-for="(img,imgindex) in item.data" :key="'imageSingle'+imgindex" :style="{padding:item.style.paddingTop+'px '+item.style.paddingLeft+'px  0px',}">
                  <img :src="img.imgUrl">
                </div>
              </div>
              <div v-if="item.type=='video'" class="my-video" :style="{padding:item.style.paddingTop+'px  0px',}">
                <video :src="item.params.videoUrl" :poster="item.params.poster" controls="controls" :style="{height:item.style.height+'px'}"></video>
              </div>
              <div v-if="item.type=='navBar'" class="my-navBar" :style="{background:item.style.background,margin:'0px '+item.style.marginLeft+'px'}" style="border-radius: 3px;">
                <p class="item-text am-text-truncate" v-if="item.params.visible" :style="{color:item.params.color,textAlign:item.params.textAlign}">
                  {{item.params.title}}
                </p>
                <ul :class="['am-avg-sm-'+item.style.rowsNum]">
                  <li v-for="(item,index) in item.data" :key="'navBar'+index" class="my-navBar-li">
                    <div class="item-image">
                      <img :src="item.imgUrl">
                    </div>
                    <p class="item-text am-text-truncate" :style="{color:item.color}">
                      {{item.text}}
                    </p>
                  </li>
                </ul>
              </div>
              <div v-if="item.type=='article'" class="my-article">
                <div class="article-item" v-for="(item,index) in item.defaultData" :key="index">
                  <div class="article-item__image">
                    <img :src="item.image">
                  </div>
                  <div class="article-item__right">
                    <div class="article-item__title twolist-hidden">
                      {{item.article_title}}
                    </div>
                    <div class="article-item__coont">
                      {{item.article_cont}}
                    </div>
                    <div class="article-item__footer">
                      {{item.date}}
                    </div>
                  </div>
                </div>
              </div>
              <div v-if="item.type=='special'" class="my-special">

              </div>
              <div v-if="item.type=='search'" class="my-search">

              </div>
              <div v-if="item.type=='notice'" class="my-notice" :style="{padding:item.style.paddingTop+'px 10px',background:item.style.background}">
                <div class="notice__icon">
                  <img :src="item.params.icon">
                </div>
                <div class="notice__text">
                  <span :style="{color:item.style.textColor}">{{item.params.text}}</span>
                </div>
              </div>

              <div v-if="item.type=='guide'" class="my-guide" :style="{padding:item.style.paddingTop+'px 0px',background:item.style.background}">
                <p class="line" :style="{borderTop:item.style.lineHeight+'px '+item.style.lineStyle+' '+item.style.lineColor}"></p>
              </div>
              <div v-if="item.type=='blank'" class="my-blank" :style="{height:item.style.height+'px',background:item.style.background}">

              </div>
              <div class="btn-edit-del" v-if="item.type!='service'">
                <div class="btn-edit-del" @click.stop="delitem(item,index)">
                  删除
                </div>
              </div>

            </div>
            <div v-if="item.type=='service'" @click="changitem(item,index)" class="my-service drag optional " :class="{selected:selectindex==index}" :style="{right:item.style.right+'%',bottom:item.style.bottom+'%',opacity:item.style.opacity/100}" :key="index">
              <div class="service-icon">
                <img :src="item.params.image">
              </div>
              <div class="btn-edit-del">
                <div class="btn-edit-del" @click.stop="delitem(item,index)">
                  删除
                </div>
              </div>
            </div>
          </template>
        </draggable>
      </div>
    </div>
  </div>
</template>

<script>
import draggable from 'vuedraggable'
export default {
  name: '',
  model: {
    prop: 'pages',
  },
  props: {
    pages: {
      type: [Array, Object],
      default() {
        return {
          page: {
            type: 'page',
            name: '页面设置',
            params: {
              title: '',
              share_title: '',
            },
            style: {
              titleTextColor: '',
              titleBackgroundColor: '',
              pageBackgroundColor: '',
            },
          },
          items: [],
        }
      },
    },
  },
  data() {
    return {
      selected: '',
      selectindex: -1,
      selectitem: {
        type: '',
      },
    }
  },
  components: {
    draggable,
  },
  created() {},
  methods: {
    //选中模块
    changitem(val, index) {
      this.selectitem = val
      this.selectindex = index
      this.chang()
    },
    dragstart(evt) {
      this.selectindex = evt.oldIndex
      this.chang()
    },
    dragend(evt) {
      this.selectindex = evt.newIndex
      this.chang()
    },
    delitem(item, index) {
      this.$confirm('确认删除吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning',
      })
        .then(() => {
          this.pages.items.splice(index, 1)
          this.selected = ''
          this.$emit('modulechang', { type: '' })
        })
        .catch(() => {})
    },
    chang() {
      this.$emit('modulechang', this.selectitem)
    },
  },
}
</script>

<style lang="scss" scoped>
.my-phone {
  width: 377px;
  border-radius: 3px;
  box-shadow: 0 3px 10px #dcdcdc;
  border: 1px solid #ddd;
  .phone-top {
    width: 100%;
    height: 66px;
    text-align: center;
    background: url('../../assets/phone-top-black.png') center center / contain
      no-repeat;
    h4 {
      margin: 0;
      font-size: 1.4rem;
      font-weight: normal;
      white-space: nowrap;
      line-height: 88px;
    }
  }
  .optional {
    position: relative;
    &:before {
      content: ' ';
    }
  }
  .optional:hover:before {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    border: 2px dashed #00a0e9;
    cursor: move;
  }
  .optional.selected:before {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    border: 2px dashed #00a0e9;
    cursor: move;
  }
  .phone-main {
    position: relative;
    border-top: 0;
    user-select: none;
    line-height: normal;
    .my-service {
      position: absolute;
      z-index: 999;
      .service-icon {
        padding: 5px;
        img {
          display: block;
          width: 45px;
          height: 45px;
        }
      }
    }
    .dragArea {
      overflow-y: auto;
      height: 580px;
      .drag {
        &:hover {
          .btn-edit-del {
            display: block;
          }
        }
        .btn-edit-del {
          height: 16px;
          position: absolute;
          right: 2px;
          bottom: 2px;
          display: none;
          z-index: 90;
          > div {
            width: 32px;
            height: 16px;
            line-height: 16px;
            display: inline-block;
            text-align: center;
            font-size: 10px;
            color: #fff;
            background: rgba(0, 0, 0, 0.4);
            margin-left: 2px;
            cursor: pointer;
            position: relative;
            z-index: 11;
          }
        }
        .my-banner {
          display: block;
          margin: 0;
          padding: 0;
          height: auto;
          overflow: hidden;
          img {
            width: 100%;
            display: none;
          }
          img:first-child {
            display: block;
          }
        }
        .my-imageSingle {
          display: block;
          margin: 0;
          padding: 0;
          height: auto;
          overflow: hidden;
          img {
            width: 100%;
            display: block;
          }
        }
        .my-video {
          video {
            display: block;
            width: 100%;
            height: 100%;
          }
        }
        .my-article {
          background: #f7f7f7;
          .article-item {
            display: flex;
            margin-bottom: 10px;
            padding: 15px;
            background: #fff;
            .article-item__right {
              padding-left: 10px;
              .article-item__title {
                min-height: 30px;
                text-overflow: ellipsis;
                overflow: hidden;
              }
              .article-item__coont {
                min-height: 30px;
                text-overflow: ellipsis;
                overflow: hidden;
              }
              .item__footer {
                margin-top: 5px;
              }
            }
            .article-item__image img {
              display: block;
              width: 120px;
            }
          }
        }
        .my-notice {
          padding: 4px 10px;
          line-height: 26px;
          display: flex;
          .notice__icon {
            font-size: 0;
            img {
              width: 16px;
              height: 16px;
              vertical-align: middle;
              border: 0;
            }
          }
          .notice__text {
            padding-left: 5px;
            flex: 1;
            word-wrap: normal;
            text-overflow: ellipsis;
            white-space: nowrap;
            overflow: hidden;
          }
        }
        .my-guide {
          .line {
            height: 0;
            width: 100%;
            margin: 0;
          }
        }
        .my-blank {
        }
      }
      .selected {
        .btn-edit-del {
          display: block;
        }
      }
      .my-navBar {
        .my-navBar-li {
          margin: 10px 0;
          .item-text {
            text-align: center;
          }
          .item-image {
            text-align: center;
            margin-bottom: 4px;
            img {
              height: 44px;
              width: 44px;
            }
          }
        }
        ul,
        li {
          list-style: none;
          padding: 0;
          margin: 0;
        }
      }
    }
  }
}

.am-ellipsis,
.am-text-truncate {
  word-wrap: normal;
  text-overflow: ellipsis;
  white-space: nowrap;
  overflow: hidden;
}
.am-avg-sm-3 > li:nth-of-type(3n + 1) {
  clear: both;
}
.am-avg-sm-3 > li:nth-of-type(n) {
  clear: none;
}
.am-avg-sm-3 > li {
  width: 33.33333333%;
}
.am-avg-sm-4 > li:nth-of-type(4n + 1) {
  clear: both;
}

.am-avg-sm-4 > li:nth-of-type(n) {
  clear: none;
}
.am-avg-sm-4 > li {
  width: 25%;
}
.am-avg-sm-5 > li:nth-of-type(5n + 1) {
  clear: both;
}

.am-avg-sm-5 > li:nth-of-type(n) {
  clear: none;
}
.am-avg-sm-5 > li {
  width: 20%;
}
[class*='am-avg-'] > li {
  display: block;
  height: auto;
  float: left;
}
[class*='am-avg-']:after,
[class*='am-avg-']:before {
  content: ' ';
  display: table;
}
[class*='am-avg-']:after {
  clear: both;
}
[class*='am-avg-']:after,
[class*='am-avg-']:before {
  content: ' ';
  display: table;
}
[class*='am-avg-'] {
  display: block;
  padding: 0;
  margin: 0;
  list-style: none;
}
</style>

三、右边组件

1.配置组件参数

<template>
  <div class="my-editor form-horizontal" v-show="data.type">
    <!-- 页面设置 -->
    <template v-if="data.type=='page'">
      <div class="editor-title">
        页面设置
      </div>
      <el-form label-width="100px" size="mini">
        <el-form-item label="页面标题">
          <el-input v-model="data.params.title"></el-input>
          <span class="help-block">小程序端顶部显示的标题</span>
        </el-form-item>
        <el-form-item label="分享标题">
          <el-input v-model="data.params.share_title"></el-input>
          <span class="help-block">小程序端转发时显示的标题</span>
        </el-form-item>
        <el-form-item label="标题栏文字">
          <el-radio-group v-model="data.style.titleTextColor">
            <el-radio label="black">黑色</el-radio>
            <el-radio label="white">白色</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="标题栏背景">
          <el-color-picker v-model="data.style.titleBackgroundColor"></el-color-picker>
        </el-form-item>
        <el-form-item label="页面背景">
          <el-color-picker v-model="data.style.pageBackgroundColor"></el-color-picker>
        </el-form-item>
      </el-form>
    </template>
    <!-- 图片轮播 -->
    <template v-if="data.type=='banner'">
      <div class="editor-title">
        图片轮播
      </div>
      <el-form label-width="100px" size="mini">
        <el-form-item label="切换时间">
          <el-input-number v-model="data.params.interval" controls-position="right" :min="1000" style="width:100%"></el-input-number>
          <span class="help-block">轮播图自动切换的间隔时间,单位:毫秒</span>
        </el-form-item>
        <el-form-item label="上下边距">
          <el-slider v-model="data.style.paddingTop" :min="0" :max="50"></el-slider>{{data.style.paddingTop}}px(像素)
        </el-form-item>
        <el-form-item label="左右边距">
          <el-slider v-model="data.style.paddingLeft" :min="0" :max="50"></el-slider>{{data.style.paddingLeft}}px(像素)
        </el-form-item>
        <!-- <el-form-item label="背景颜色模式">
          <el-radio-group v-model="data.style.backgroundTpye">
            <el-radio label="all">全部填充</el-radio>
            <el-radio label="top">上下</el-radio>
          </el-radio-group>
        </el-form-item> -->
        <el-form-item label="背景颜色">
          <el-color-picker v-model="data.style.background"></el-color-picker>
        </el-form-item>
        <div class="form-items">
          <div class="form-item" v-for="(img,imgindex) in data.data" :key="'banner'+imgindex">
            <i class="el-icon-close item-delete" @click.stop="delimgitem(data.data,imgindex)"></i>
            <div class="item-inner">
              <el-form-item label="图片">
                <div class="data-image" @click.stop="showfile(imgindex,data.type)">
                  <img :src="img.imgUrl">
                </div>
                <span class="help-block">建议尺寸750x360</span>
              </el-form-item>
              <el-form-item label="链接地址">
                <el-input v-model="img.linkUrl"></el-input>
              </el-form-item>
            </div>
          </div>
        </div>
        <div class="form-item-add" @click.stop="addimgitem(data.data)">
          添加一个
        </div>
      </el-form>
    </template>
    <!-- 单组图片 -->
    <template v-if="data.type=='imageSingle'">
      <div class="editor-title">
        单组图片
      </div>
      <el-form label-width="100px" size="mini">
        <el-form-item label="上下边距">
          <el-slider v-model="data.style.paddingTop" :min="0" :max="50"></el-slider>{{data.style.paddingTop}}px(像素)
        </el-form-item>
        <el-form-item label="左右边距">
          <el-slider v-model="data.style.paddingLeft" :min="0" :max="50"></el-slider>{{data.style.paddingLeft}}px(像素)
        </el-form-item>
        <el-form-item label="背景颜色">
          <el-color-picker v-model="data.style.background"></el-color-picker>
        </el-form-item>
        <div class="form-items">
          <div class="form-item" v-for="(img,imgindex) in data.data" :key="'banner'+imgindex">
            <i class="el-icon-close item-delete" @click.stop="delimgitem(data.data,imgindex)"></i>
            <div class="item-inner">
              <el-form-item label="图片">
                <div class="data-image" @click.stop="showfile(imgindex,data.type)">
                  <img :src="img.imgUrl">
                </div>
              </el-form-item>
              <el-form-item label="链接地址">
                <el-input v-model="img.linkUrl"></el-input>
              </el-form-item>
            </div>
          </div>
        </div>
        <div class="form-item-add" @click.stop="addimgitem(data.data)">
          添加一个
        </div>
      </el-form>
    </template>
    <!-- 视频组件 -->
    <template v-if="data.type=='video'">
      <div class="editor-title">
        视频组
      </div>
      <el-form label-width="100px" size="mini">
        <el-form-item label="上下边距">
          <el-slider v-model="data.style.paddingTop" :min="0" :max="50"></el-slider>{{data.style.paddingTop}}px(像素)
        </el-form-item>
        <el-form-item label="视频高度">
          <el-slider v-model="data.style.height" :min="0" :max="500"></el-slider>{{data.style.height}}px(像素)
        </el-form-item>
        <el-form-item label="视频封面">
          <div class="data-image" @click.stop="showfile(-1,data.type)">
            <img :src="data.params.poster">
          </div>
        </el-form-item>
        <el-form-item label="视频地址">
          <el-input v-model="data.params.videoUrl"></el-input>
        </el-form-item>
        <el-form-item label="自动播放">
          <el-radio-group v-model="data.params.autoplay">
            <el-radio label="0"></el-radio>
            <el-radio label="1"></el-radio>
          </el-radio-group>
        </el-form-item>
      </el-form>
    </template>
    <!-- 导航组 -->
    <template v-if="data.type=='navBar'">
      <div class="editor-title">
        导航组
      </div>
      <el-form label-width="100px" size="mini">
        <el-form-item label="是否显示标题">
          <el-radio-group v-model="data.params.visible">
            <el-radio :label="1">显示</el-radio>
            <el-radio :label="0">隐藏</el-radio>
          </el-radio-group>
        </el-form-item>
        <template v-if="data.params.visible">
          <el-form-item label="标题文字">
            <el-input v-model="data.params.title"></el-input>
          </el-form-item>
          <el-form-item label="文字对齐">
            <el-radio-group v-model="data.params.textAlign">
              <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-item label="标题文字验证">
            <el-color-picker v-model="data.params.color"></el-color-picker>
          </el-form-item>
        </template>
        <el-form-item label="左右边距">
          <el-slider v-model="data.style.marginLeft" :min="0" :max="50"></el-slider>{{data.style.paddingLeft}}px(像素)
        </el-form-item>
        <el-form-item label="背景颜色">
          <el-color-picker v-model="data.style.background"></el-color-picker>
        </el-form-item>
        <el-form-item label="每行数量">
          <el-radio-group v-model="data.style.rowsNum">
            <el-radio :label="3">3</el-radio>
            <el-radio :label="4">4</el-radio>
            <el-radio :label="5">5</el-radio>
          </el-radio-group>
        </el-form-item>
        <div class="form-items">
          <draggable v-model="data.data">
            <div class="form-item" v-for="(img,imgindex) in data.data" :key="'banner'+imgindex">
              <i class="el-icon-close item-delete" @click.stop="delimgitem(data.data,imgindex)"></i>
              <div class="item-inner">
                <el-form-item label="图片">
                  <div class="data-image" @click.stop="showfile(imgindex,data.type)">
                    <img :src="img.imgUrl">
                  </div>
                  <span class="help-block">建议尺寸100x100</span>
                </el-form-item>
                <el-form-item label="文字内容">
                  <el-input v-model="img.text"></el-input>
                </el-form-item>
                <el-form-item label="文字颜色">
                  <el-color-picker v-model="img.color"></el-color-picker>
                </el-form-item>
                <el-form-item label="业务分类">
                  <SelectTree :props="bprops" :options="business" :data.sync="img.classify" :clearable="true" :accordion="true" height="200" style="width:100%"></SelectTree>
                </el-form-item>
                <el-form-item label="链接地址">
                  <el-input v-model="img.linkUrl"></el-input>
                </el-form-item>
              </div>
            </div>
          </draggable>
        </div>
        <div class="form-item-add" @click.stop="addimgitem(data.data)">
          添加一个
        </div>
      </el-form>
    </template>
    <!-- 文章组 -->
    <template v-if="data.type=='article'">
      <div class="editor-title">
        文章组
      </div>
      <el-form label-width="100px" size="mini">
        <el-form-item label="文章分类">
          <SelectTree :props="props" :options="trredata" :data.sync="data.params.auto.category" :clearable="true" :accordion="true" height="200" style="width:100%"></SelectTree>
        </el-form-item>
        <el-form-item label="显示数量">
          <el-input-number v-model="data.params.auto.showNum" placeholder="请输入显示数量" :min="1" controls-position="right" style="width:100%"></el-input-number>
        </el-form-item>
      </el-form>
    </template>
    <!-- 公告组 -->
    <template v-if="data.type=='notice'">
      <div class="editor-title">
        公告组
      </div>
      <el-form label-width="100px" size="mini">
        <el-form-item label="上下边距">
          <el-slider v-model="data.style.paddingTop" :min="0" :max="50"></el-slider>{{data.style.paddingTop}}px(像素)
        </el-form-item>
        <el-form-item label="背景颜色">
          <el-color-picker v-model="data.style.background"></el-color-picker>
        </el-form-item>
        <el-form-item label="文字颜色">
          <el-color-picker v-model="data.style.textColor"></el-color-picker>
        </el-form-item>
        <el-form-item label="公告图标">
          <div class="data-image" @click.stop="showfile(-1,data.type)">
            <img :src="data.params.icon" style="height: 30px;">
            <span class="help-block">建议尺寸32x32</span>
          </div>
        </el-form-item>
        <el-form-item label="公告内容">
          <el-input v-model="data.params.text"></el-input>
        </el-form-item>
      </el-form>
    </template>
    <!-- 辅助空白 -->
    <template v-if="data.type=='blank'">
      <div class="editor-title">
        辅助空白
      </div>
      <el-form label-width="100px" size="mini">
        <el-form-item label="组件高度">
          <el-slider v-model="data.style.height" :min="0" :max="50"></el-slider>{{data.style.height}}px(像素)
        </el-form-item>
        <el-form-item label="背景颜色">
          <el-color-picker v-model="data.style.background"></el-color-picker>
        </el-form-item>
      </el-form>
    </template>
    <!-- 辅助线 -->
    <template v-if="data.type=='guide'">
      <div class="editor-title">
        辅助线
      </div>
      <el-form label-width="100px" size="mini">
        <el-form-item label="背景颜色">
          <el-color-picker v-model="data.style.background"></el-color-picker>
        </el-form-item>
        <el-form-item label="线条样式">
          <el-radio-group v-model="data.style.lineStyle">
            <el-radio label="solid">实线</el-radio>
            <el-radio label="dashed">虚线</el-radio>
            <el-radio label="dotted">点状</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="线条颜色">
          <el-color-picker v-model="data.style.lineColor"></el-color-picker>
        </el-form-item>
        <el-form-item label="线条高度">
          <el-slider v-model="data.style.lineHeight" :min="0" :max="20"></el-slider>{{data.style.lineHeight}}px(像素)
        </el-form-item>
        <el-form-item label="上下边距">
          <el-slider v-model="data.style.paddingTop" :min="0" :max="50"></el-slider>{{data.style.paddingTop}}px(像素)
        </el-form-item>
      </el-form>
    </template>
    <!-- 在线客服 -->
    <template v-if="data.type=='service'">
      <div class="editor-title">
        在线客服
      </div>
      <el-form label-width="100px" size="mini">
        <el-form-item label="底边距">
          <el-slider v-model="data.style.bottom" :min="0" :max="100"></el-slider>{{data.style.height}}%
        </el-form-item>
        <el-form-item label="右边距">
          <el-slider v-model="data.style.right" :min="0" :max="100"></el-slider>{{data.style.height}}%
        </el-form-item>
        <el-form-item label="不透明度">
          <el-slider v-model="data.style.opacity" :min="0" :max="100"></el-slider>{{data.style.opacity}}%
        </el-form-item>
        <el-form-item label="线条样式">
          <el-radio-group v-model="data.params.type">
            <el-radio label="chat">在线聊天</el-radio>
            <el-radio label="phone">拨打电话</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="电话号码" v-if="data.params.type=='phone'">
          <el-input v-model="data.params.phone_num" placeholder="请输入电话号码" :maxlength='11' type="tel"></el-input>
        </el-form-item>
        <el-form-item label="客服图标">
          <div class="data-image" @click.stop="showfile(-1,data.type)">
            <img :src="data.params.image" style="height: 30px;">
            <span class="help-block">建议尺寸90x90</span>
          </div>
        </el-form-item>
      </el-form>
    </template>
    <FileLibrary :visible.sync="FileLibraryvisible" @ok="Fileok"></FileLibrary>
  </div>
</template>

<script>
import FileLibrary from '../FileLibrary'
import SelectTree from '../treeSelect'
import draggable from 'vuedraggable'
export default {
  name: '',
  model: {
    prop: 'data',
  },
  props: {
    data: {
      type: [Object, Array],
      default() {
        return {
          type: '',
        }
      },
    },
  },
  data() {
    return {
      dataindex: -1,
      FileLibraryvisible: false,
      ntype: '',
      trredata: [
        {
          id: 0,
          label: '全部分类',
          children: [],
        },
      ],
      props: {
        // 配置项(必选)
        value: 'id',
        label: 'label',
        children: 'children',
        // disabled:true
      },
      bprops: {
        // 配置项(必选)
        value: 'id',
        label: 'name',
        children: 'children',
      },
      business: [],
    }
  },
  components: {
    FileLibrary,
    SelectTree,
    draggable,
  },
  created() {
  
  },
  methods: {
    addimgitem(data) {
      data.push({
        linkUrl: '',
        imgUrl: '',
      })
    },
    delimgitem(data, index) {
      data.splice(index, 1)
    },
    showfile(index, type) {
      this.FileLibraryvisible = true
      this.dataindex = index
      this.ntype = type
    },
    Fileok(val) {
      if (val.length != 0) {
        switch (this.ntype) {
          case 'banner':
          case 'imageSingle':
          case 'navBar':
            this.data.data[this.dataindex].imgUrl = val[0]
            break
          case 'video':
            this.data.params.poster = val[0]
            break
          case 'notice':
            this.data.params.icon = val[0]
            break
          case 'service':
            this.data.params.image = val[0]
            break
        }
      }
    },
  },
}
</script>

<style lang="scss" scoped>
.my-editor {
  width: 400px;
  height: auto;
  min-height: 100px;
  padding: 15px 10px;
  border: 1px solid #ddd;
  .data-image {
    display: inline-block;
    width: auto;
    min-width: 40px;
    max-width: 220px;
    text-align: center;
    cursor: pointer;
  }
  .editor-title {
    position: relative;
    padding: 0 22px;
    height: 34px;
    border-bottom: 1px solid #eef1f5;
    margin-bottom: 20px;
    line-height: 34px;
    &:before {
      content: '';
      position: absolute;
      width: 4px;
      height: 13px;
      background: #00aeff;
      top: 10px;
      left: 9px;
    }
  }
  .tpl-form-line-form {
    font-size: 1.3rem !important;
    color: #656565;
  }
  .help-block {
    color: #838fa1;
    font-size: 12px;
  }
  .form-items {
    height: auto;
    padding: 5px 6px;
    .form-item {
      background: #f7fafc;
      margin-bottom: 0.6rem;
      position: relative;
      border-radius: 3px;
      cursor: move;
      .item-delete {
        position: absolute;
        top: -6px;
        right: -6px;
        height: 16px;
        width: 16px;
        line-height: 16px;
        background: rgba(153, 153, 153, 0.7);
        color: #fff;
        border-radius: 50%;
        text-align: center;
        cursor: pointer;
        font-size: 12px;
        transition: background-color 0.3s ease-out, border-color 0.3s ease-out;
      }
      .item-inner {
        padding: 10px;
        background: #f7fafc;
        span {
          display: block;
          width: 100%;
        }
      }
    }
  }
  .form-item-add {
    width: 100%;
    background: #fdfdfd;
    color: #6b6b6b;
    border: 1px solid #efefef;
    outline: none;
    padding: 10px 16px;
    border-radius: 2px;
    font-size: 12px;
    line-height: 1;
    text-align: center;
    vertical-align: middle;
    cursor: pointer;
    user-select: none;
    transition: All 0.2s ease-in-out;
  }

  .data-image {
    display: inline-block;
    width: auto;
    min-width: 40px;
    max-width: 220px;
    text-align: center;
    cursor: pointer;
    img {
      display: block;
      max-width: 100%;
      height: 50px;
    }
  }
}
</style>

四、主组件

<template>
  <el-row :gutter="20">
    <el-col :span="8">
      <LeftPage @save="save" @add="addcomponent"></LeftPage>
    </el-col>
    <el-col :span="8">
      <MainPage :pages="pages" @modulechang="modulechang"></MainPage>
    </el-col>
    <el-col :span="8">
      <RightPage :data="selectitem"></RightPage>
    </el-col>
  </el-row>
</template>

<script>
import LeftPage from './left'
import MainPage from './main'
import RightPage from './right'
export default {
  name: '',
  props: {
    data: {
      type: [Object],
      default() {
        return {}
      },
    },
  },
  data() {
    return {
      pages: this.data,
      selected: '',
      selectindex: -1,
      selectitem: {
        type: '',
      },
    }
  },
  components: {
    LeftPage,
    MainPage,
    RightPage,
  },
  created() {},
  watch: {
    'data.page.type'() {
      this.pages = this.data
    },
  },
  methods: {
    modulechang(val) {
      this.selectitem = val
    },
    addcomponent(item) {
      this.pages.items.push(item)
    },
    save() {
      this.$emit('save', this.pages)
    },
  },
}
</script>

<style lang="scss" scoped>
</style>

下期会分享动态配置表单配置