当前位置: 首页 > 工具软件 > iView-Aliapp > 使用案例 >

vue 表单封装(利用iview自带组件)

宗项禹
2023-12-01

实战代码功能如下

1.inputNumber  数字框 千分位和小数点 处理。(详细功能会另起文章)

2.AutoComplete 和select 框的联动处理

3. 弹框功能和弹框表单

4.动态添加表单字段

5.实战富文本编辑器另起文档说明。

 

封装目的和解决痛点,

表单字段 10几个20个时候,需要人为的不断拷贝和添加  相同的代码 特别是iview  冗余代码会非常多,html也不方便维护

本篇文章提供的代码是 

主要实现弹框的form 表单 且 有联动查询,各个组件会统一提交。


 注意: 核心 配置 在termOption.js   表单的验证 为了大家交流学习本人以不同的形式v-bind   并不是代码不规范,请勿吐槽

inpuNumber  在iview.vue 中是有部分问题的。 

代码已做优化,  利用 inputFocus 事件 和绑定验证的blur 和change 做处理    具体实战我会在vue  实战系列单独列出。这里不做详细说明。


代码封装 form表单组件 (CusForm.vue

源码如下:

<template>
  <div class="formPanel">
    <FormItem v-for="(item) in datas" :key="item.prop" :label="item.label" :rules="item.rules" :prop="item.prop" :class="item.class">
      <i-input v-if="item.type === 'input'" v-model.trim="formModel[item.model]" :type="item.inputType" :disabled="item.disabled" :placeholder="item.placeholder" :size="item.size" />
      <RadioGroup v-if="item.type === 'radio'" v-model="formModel[item.model]">
        <Radio v-for="(e, i) in item.children" :key="i" :label="e.label">
          {{ e.text }}
        </Radio>
        <slot v-if="item.slot" :name="item.slot" />
      </RadioGroup>
      <CheckboxGroup v-if="item.type === 'checkbox'" v-model="formModel[item.model]" class="mycheck-group">
        <Checkbox v-for="e in item.children" :key="e.label" :label="e.label" :disabled="e.disabled">
          {{ e.text }}
        </Checkbox>
      </CheckboxGroup>
      <Checkbox v-if="item.type === 'check'" v-model="formModel[item.model]" :label="item.checklabel" :disabled="e.disabled">
        {{ e.text }}
        <slot v-if="item.slot" :name="item.slot" />
      </Checkbox>
      <DatePicker v-if="item.type === 'datePicker'" v-model="formModel[item.model]" style="width:100%;" :value="formModel[item.model]" :type="item.pickertype ? item.pickertype : 'date'" :options="item.options" :transfer="item.transfer" :format ="item.format" :size="item.size" :placeholder="item.placeholder" />
      <i-input v-if="item.type === 'textarea'" v-model="formModel[item.model]" :autosize="item.autosize"  :placeholder="item.placeholder"  type="textarea" :size="item.size" />
      <Select v-if="item.type === 'select'" v-model="formModel[item.model]" :disabled="item.disabled" :filterable="item.filterable" :size="item.size" :placeholder="item.placeholder">
        <Option v-for="e in item.children" :key="e.label" :value="e.label">
          {{ e.text }}
        </Option>
      </Select>
      <label v-if="item.type === 'text'" :size="item.size">
        {{ item.text }} <slot v-if="item.slot" :name="item.slot" :params="item.params" />
      </label>
      <InputNumber
        v-if="item.type==='number'"
        v-model="formModel[item.model]"
        style="width:100%;"
        :formatter="value => `${value}`.replace(/(?=(?!\b)(\d{3})+\.)/g,',')"
        :parser="value => value.replace(/$s?|(,*)/g, '')" :precision="item.precision" :min="item.min"
        :max="item.max"
        :disabled="item.disabled"
        :placeholder="item.placeholder"
        :size="item.size" />
      <Input v-if="item.type==='number2'" v-model="formModel[item.model]" :maxlength="16" placeholder="" type="text" @on-focus="inputFocus(item.model)" />
      
      <div v-if="item.type === 'inputSlot'" style="postion:relative;">
        <i-input v-model="formModel[item.model]" :type="item.inputType" :disabled="item.disabled" :placeholder="item.placeholder" :size="item.size" />
        <slot v-if="item.slot" :name="item.slot" :params="item.params" />
      </div>


      <auto-comp style="width:100%;"
            v-if="item.type === 'autoComp'"
            :placeholder="item.placeholder"
            v-model.trim="formModel[item.model]"
            @on-search="autoCompSearch(item.eventName)"
            @on-focus="autoCompSearch(item.eventName)"
          >
            <Option
              v-for="e in item.children"
              :key="e.text" :value="e.text"
            >{{ e.text }}</Option>
          </auto-comp>
    </FormItem>
  </div>
</template>
<script>
import AutoComp from '@comp/widgets/AutoCompLinkage'

export default {
  name: 'CusForm',
  components: {
    AutoComp
  },
  props: {
    datas: {
      type: Array,
      required: true
    },
    formModel: {
      type: Object,
      required: true
    }
  },
  data () {
    return {
      left: ''
    }
  },
  computed: {
  },
  watch: {
  },
  mounted () {
    // this.left = this.toLeft !== '1' ? this.toLeft : (this.inline ? '0' : '200')
  },
  methods: {
    autoCompSearch (eventName) {
      this.$emit('autoSearch', eventName)
    },
    inputFocus (name) {
      this.$emit('inputFocus', name)
    }
  }
}
</script>
<style lang="scss">
.formPanel {

   .ui-form-item {
     margin-bottom: 24px;

     .ui-form-item-content {
       width: 264px;
     }

    &.optionBtn {
      margin: -15px 0 0 0;
    }

    &.contractName{
      height: 82px;
      margin-bottom: 15px;
    }

     &.middleSize {

       .ui-form-item-content {
         width: 429px;

         textarea {
           height: 66px;
         }
       }
     }

     &.lineMiddleSize {
       .ui-form-item-content {
         width: 300px;

         textarea {
           height: 145px;
         }
       }
     }
   }
   .ui-select {
     width:264px;
   }
}
.form-wrap {

  .ivu-cascader-rel {
    width: 18.75rem;
  }

  .phone-wrap {

    .phone-zone {
      width: 3.75rem;

      .ivu-input {
        width: 3.75rem !important;
      }
    }

    .phone-num {
      width: 14.062rem;

      .ivu-input {
        width: 14.062rem !important;
      }
    }
  }
}

.mycheck-group {
  overflow: hidden;

  .ivu-checkbox-wrapper {
    display: flex;
    float: left;
    align-items: center;
    margin-right: 0;
    min-width: 20%;
    font-size: 14px;

    > span {
      margin-right: 9px;
    }
  }
}
</style>
<style lang="scss" scoped>
.form-wrap {

  &.formvertical {

    .btn-wrap {
      margin-left: 8.125rem;
    }
  }
  margin-top: 12px;

  .btn-wrap {
    display: inline-block;
    margin-top: 4px;
  }

  .btncenter {
    display: flex;
    justify-content: center;
    margin-top: 1.875rem;
  }
}

</style>

 


AutoCompLinkage.vue   源码 再 CusForm中的 引用

<template>
  <AutoComplete class="search" ref="child" v-model.trim="autoValue" :icon="icon" :placeholder="placeholder" clearable style="width: 240px;"
    @on-search="handleSearch"
    @on-change="handleChange"
    @on-focus="handleFocus"
    @on-select="handleSelect"
  >
<!--     <div class="empty" v-if="isEmpty">暂无搜索结果</div>
    <Option v-else v-for="elem in elemsList" :value="elem.templateName" :key="elem.id">{{ elem.templateName }}</Option> -->
    <slot></slot>
  </AutoComplete>
</template>

<script>
// 两个autoComp联动时使用,即:一个清空时,另一个也清空
// 若只需使用一个autoComp,或者两个相互独立没有联动,请使用'./AutoComp.vue'
export default {
  name: 'autocomplete-linkage',

  data () {
    return {
      isTyping: false,
      checkerTimeout: null,
      lastQuery: '',
      autoValue: ''
    }
  },

  props: {
    value: {
      type: [String, Number]
    },
    placeholder: {
      type: String,
      default: '请输入'
    },
    icon: null
    // styleObj: {
    //   type: Object,
    //   default: {
    //     width: 240
    //   }
    // }
  },

  methods: {
    delay () {
      // 防抖
      if (this.checkerTimeout !== null) {
        clearTimeout(this.checkerTimeout)
      }
      this.checkerTimeout = setTimeout(() => {
        this.isTyping = false
      }, 500)
    },
    handleSearch (query = '') {
      query = query.trim()
      this.lastQuery = query

      if (this.isTyping) {
        this.delay()
        return
      }

      this.$emit('on-search', query)
      if (this.value) {
        setTimeout(() => {
          this.$nextTick(() => {
            this.$refs['child'].$refs.input.focus()
            // document.querySelector('.insert-search [autocomplete]').focus()
          })
        }, 0)
      }
    },
    handleChange (value) {
      this.isTyping = true
      if (this.lastQuery === '') {
        this.handleSearch(value)
      }
      if (this._isNil(value)) {
        console.log('this.lastQuery: ', this.lastQuery)
        value = this.lastQuery
        this.autoValue = this.lastQuery
      }
      if (!value.endsWith(' ')) {
        this.autoValue += ' '
      }
      this.$emit('on-change', this.autoValue)
    },
    handleFocus (event) {
      // this.handleSearch(this.autoValue)
      this.$emit('on-focus', event)
      // 若需要focus即查询,在父组件加'on-focus'
    },
    handleSelect (value) {
      this.$nextTick(() => {
        if (!this._isNil(value)) {
          this.$emit('on-select', this.autoValue)
        } else {
          if (this.value) {
            this.$refs['child'].$refs.input.focus()
          }
          // let auto = document.querySelector('.insert-search [autocomplete]')
          // auto.focus()
        }
      })
    }
  },

  watch: {
    isTyping (newValue, oldValue) {
      if (!newValue) {
        this.handleSearch(this.autoValue)
      }
    },
    value (newVal) {
      this.lastQuery = newVal
      this.autoValue = newVal
    },
    autoValue (newVal) {
      this.$emit('input', newVal)
    }
  }
}
</script>

<style lang="scss" scoped>
.col-left {
  padding-right: 10px;
  line-height: 30px;
  font-size: 14px;
  text-align: right;
  color: #333;

  &:after {
    content: ':'
  }
}
.empty{
  height: 30px;
  margin:10px 0 0 15px;
  color: #999;
}
</style>

 


二次封装 弹框form 实战。

 

弹框源码 tipModal.vue 

和 modpage.js  的代码

 

export default {
  props: {
    value: Boolean
  },

  data () {
    return {
      visible: false,
      loading: true
    }
  },

  watch: {
    value (newVal) {
      this.visible = newVal
    },
    visible (newVal) {
      if (newVal && this.onShow) {
        this.onShow()
      }
      this.$emit('input', newVal)
    }
  }
}

/***上面是ModalPage.js 代码请放置到指定目录**/


<template>
  <Modal
     class-name="vertical-center-modal"
    :mask-closable="false"
    v-model="visible"
    :title="title"
    :width='width'
    >
    <div style="font-size: 14px;text-align:center; max-height:447px; min-height:100px; overflow-y:auto">
      <slot name="content"></slot>
    </div>
    <div slot="footer" align="middle">
      <Button type="primary" @click.stop="onOk">确定</Button>
      <Button type="ghost" @click="delCancel()">取消</Button>
    </div>
  </Modal>
</template>

<script>
import ModalMixin from '@comp/mixins/ModalPage'

export default {
  name: 'tip-modal',

  mixins: [ModalMixin],

  props: {
    title: String,
    width: {
      default: 420,
      type: Number
    },
    onOk: {
      default: () => {
        this.visible = false
      },
      type: Function
    }
  },

  data () {
    return {
    }
  },

  methods: {
    delCancel () {
      this.visible = false
    }
  }
}
</script>

<style lang="scss" scoped>

</style>

 modform.vue    组件代码

<template>
  <tip-modal v-model="tipShow" :title="title" :onOk="submit"  :width="width">
    <template slot="content">
      <Form ref="formTemp" v-if='tipShow'  :inline="true" :show-message="true" :model="infoData" :rules="rules" :label-width="labelWidth" style="text-align:left;">
        <cus-form :datas="fromFileds" :formModel="infoData" @inputFocus="inputFocus" @autoSearch='autoSearch' >
           <template v-for="(item) in fromFileds" :slot="item.slot" >
             <slot v-if="item.slot" :name="item.slot" :params="item.params" />
           </template>
        </cus-form>
      </Form>
    </template>
  </tip-modal>
</template>

<script>
import tipModal from './TipModal'
import CusForm from '@comp/widgets/CusForm'
import { setTimeout } from 'timers'
const arrayModelType = ['checkbox']
export default {
  name: 'crate-modal',

  components: {
    CusForm,
    tipModal
  },
  props: {
    labelWidth: {
      default: 262,
      type: Number
    },
    width: {
      default: 420,
      type: Number
    },
    fromFileds: {
      type: Array,
      default: () => {
        return []
      }
    },
    rules: {
      type: Object,
      default: () => {
        return {}
      }
    },
    title: String
  },
  watch: {
    tipShow () {
      this.infoData = this.setupForm()
    },
    'infoData.industry' (newVal, oldVal) {
      if (oldVal) {
        this.$set(this.infoData, 'l1Class', null)
      }
      this.changeList('industry', newVal)
    },
    'infoData.l1Class' (newVal, oldVal) {
      if (oldVal) {
        this.$set(this.infoData, 'l2Class', null)
      }
      this.changeList('l1Class', newVal)
    },
    'infoData.l2Class' (newVal, oldVal) {
      if (oldVal) {
        this.$set(this.infoData, 'l3Class', null)
      }
      this.changeList('l2Class', newVal)
    },
    'infoData.mainTag' (newVal, oldVal) {
      if (oldVal) {
        this.infoData.secTag = ''
      }
      this.autoSearch('secTagEvent')
    }

  },
  mounted () {

  },
  data () {
    return {
      fileds: [],
      tipShow: false,
      infoData: {}
    }
  },
  methods: {
    // AutoComplete 的事件联动
    autoSearch (eventName) {
      this.$emit('autoSearch', eventName)
    },

    //
    changeList (listName, value) {
      this.$emit('changeList', listName, value)
    },
    tipShowChange (value) {
      this.tipShow = value
    },
    resetFields () {
      this.$refs['formTemp'].resetFields()
    },
    inputFocus (name) { // 数字框的特殊处理
      // eslint-disable-next-line no-useless-escape
      this.signInfoData[name] = this.signInfoData[name].replace(/\,/g, '')
    },
    setupForm () { // 表单默认值处理
      let forms = {}
      this._forEach(this.fromFileds, item => {
        let arr = []
        if (item.type === 'checkbox') {
          this._forEach(item.children, o => {
            o.value && arr.push(o.value)
          })
        }
        if (!this._isNil(item.model) && item.type !== 'text') {
          // 取默认值
          forms[item.model] = !this._isNil(item.value) ? item.value : arrayModelType.includes(item.type) ? arr : ''
        }
        if (['number', 'number2'].includes(item.type) && this._isNil(item.value)) {
          item.value = 0
        }
      })
      return forms
    },
    submit () {
      this.$refs['formTemp'].validate((valid) => {
        if (valid) {
          this.$emit('handleSubmit', this.getData())
        }
      })
    },
    formatData (data) {
      // 表单要素的数据格式化处理。
      let obj = {}
      this._forEach(this.fromFileds, (item) => {
        obj[item.model] = data[item.model]
        if (item.formateValue) {
          obj[item.model] = item.formateValue(data[item.model])
        }
      })
      return obj
    },
    setData (detailInfo) {
      detailInfo = this.formatData(detailInfo)
      Object.assign(this.infoData, detailInfo)
      setTimeout(() => {
        this.$refs['formTemp'].validate()
      }, 700)
    },
    getData () {
      return this.formatData(this.infoData)
    }
  }
}
</script>

<style lang="scss" scoped>

</style>

 

实战实例点 

Create.vue    我的项目代码用于创建的,里面有一个富文本编辑器, 富文本编辑器的应用,会有文章单独讲解。此处不做说明

此处代码的实战主要用户 联动 和自动auto框 。 

<template>
  <article>
    <insert-modal v-model="isInsertModalShow" @choiceElem="insertElem"></insert-modal>
    <modal-from ref="modlfrom" title="提交条款" 
    :label-width='labelWidth' 
    :width="width"
    :from-fileds="datas"
    :rules ="rules"
    @handleSubmit="handleSubmit" @autoSearch="autoSearch" @changeList="changeList"></modal-from>
    <section class="panel-heading">
      <Button type="ghost" @click="goBackList">返回</Button>
      <span class="panel-title" :title="title">
        {{ this.title }}
      </span>
      <span class="right-panel">
        <Button type="primary" @click="showFromModal">提交</Button>
      </span>
    </section>
    <div :class="{maxDiv:id, minDiv:!id}"  class="reality">
      <tinymce id="tinymce-area" ref="tinymceArea" v-model="content" :width="942" style="width: 950px;margin: 0 auto;float: left;" @onChoose="isInsertModalShow=true"></tinymce>
      <div class="demo-panel" v-if='id'>
        <p class="exampleP">条款范例
          <span style="padding-left:5px; font-size:12px;color:#999999">(条款的应用举例)</span></p>
          <example-list ref="exampleList" :term-id="id" />
      </div>
    </div>
    <!-- <div>{{ content }}</div> -->
  </article>
</template>

<script>
import Tinymce from '@comp/widgets/TinymceVue'
import InsertModal from '../Contract/Modals/Insert'
import ModalFrom from './Modals/ModalForm'
import exampleList from './component/exampleList'
import { defaultStyles } from '@util/tinymceConfig'
import { toFreeMaker, toEditor, pickElems } from '@util/templateFormat'
import {addProjectForms} from './Modals/termOption'

export default {
  name: 'term-create-page',

  components: {
    Tinymce,
    InsertModal,
    ModalFrom,
    exampleList
  },

  data () {
    return {
      title: '',
      id: '',
      detailInfo: '',
      labelWidth: 120,
      width: 888,
      datas: [],
      list: {
        industry: [],
        l1Class: [],
        l2Class: [],
        l3Class: [],
        mainTag: [],
        secTag: []
      },
      rules: {
        mainTag: [
          { required: true, message: '请输入主标签', trigger: 'change' },
          { validator: this.mainTagValid, trigger: 'blur' }
        ],
        secTag: [
          { required: true, message: '请输入次标签', trigger: 'change' },
          { validator: this.secTagValid, trigger: 'blur' }
        ]

      },
      isInsertModalShow: false,
      content: 'content'
    }
  },

  computed: {
  },

  mounted () {
    this.datas = addProjectForms()
    this.$refs.tinymceArea.init(defaultStyles)
    this.id = this.$route.query.id
    if (this.id) { // 编辑赋值处理。
      this.showTermContent()
    } else {
      this.title = '新增条款'
    }
  },

  beforeDestroy () {

  },

  methods: {
    mainTagValid (rule, val, cb) {
      if (val && this.list.mainTag.length > 0) {
        let item = this._find(this.list.mainTag, item => {
          return item.dictName === val
        })
        if (item) {
          cb()
        } else {
          cb(new Error('请输入主标签'))
        }
      } else {
        cb(new Error('请输入主标签'))
      }
    },
    secTagValid (rule, val, cb) {
      if (val && this.list.secTag.length > 0) {
        let item = this._find(this.list.secTag, item => {
          return item.dictName === val
        })
        if (item) {
          cb()
        } else {
          cb(new Error('请输入次标签'))
        }
      } else {
        cb(new Error('请输入次标签'))
      }
    },
    autoSearch (eventName) {
      switch (eventName) {
        case 'mainTagEvent':
          this.mainTagSearch()
          break
        case 'secTagEvent':
          this.secTagSearch()
          break
      }
    },
    changeList (listName, value) {
      switch (listName) {
        case 'industry':
          this.getL1Class(value)
          this.changeDatas('l1Class')
          break
        case 'l1Class':
          this.getL2Class(value)
          this.changeDatas('l2Class')
          break
        case 'l2Class':
          this.list.l3Class = []
          this.getL3Class(value)
          this.changeDatas('l3Class')
          break
      }
    },
    changeDatas (listName) {
      let index = -1
      this.datas.map((item, i) => {
        if (item.model === listName) {
          index = i
        }
      })
      if (index !== -1) {
        let item = this.datas[index]

        // 此步骤是核心关键格式化 接口返回数据,因包含了
        // industry:"行业"
        // l1Class:"业务类型"
        // l2Class:产品类型
        // l3Class:"产品细分"
        // mainTag:"主标签"
        // secTag:"次标签"
        // 可能会出现 接口返回字段名称不一致 因接口统一无问题,如果接口不统一或者字段名称不一样请根据listName 做判断取不同的值进行格式化
        item.children = this.list[listName].map((item) => {
          let obj = {}
          obj.label = item.dictCode
          obj.text = item.dictName
          return obj
        })
        this.datas.splice(index, 1, item)
      }
    },
    queryDictList (reqData, listName) {
      let params = this._merge({escapeLoading: true}, reqData)
      this.$http.post('dict/queryDictList', params).then(res => {
        if (res.rtnCode === '000') {
          this.list[listName] = res.data || [] // 一定要先赋值, 因为在调用 changeDatas 需要用到赋值的对象
          this.changeDatas(listName)
        } else {
          this.$Message.error({content: res.rtnMsg, closable: true})
        }
      }).catch(err => {
        this.$debug(err)
      })
    },
    simplifyDictTreeList (treeNode) {
      if (this._isNil(treeNode) || treeNode.length === 0) {
        return []
      }
      return this._map(treeNode, (o) => {
        return {'dictCode': o.dictCode, 'dictName': o.dictName, 'childrenList': this.simplifyDictTreeList(o.childrenList)}
      })
    },
    getTreeListNode (parentNode, parentNodeValue) {
      let list = this._find(parentNode, {dictCode: parentNodeValue})
      return this._isNil(list) ? [] : list.childrenList
    },
    getL1Class (value) {
      this.list.l1Class = this.getTreeListNode(this.list.industry, value)
    },
    getL2Class (value) {
      this.list.l2Class = this.getTreeListNode(this.list.l1Class, value)
    },
    getL3Class (value) {
      this.list.l3Class = this.getTreeListNode(this.list.l2Class, value)
    },
    mainTagSearch (value) {
      let reqData = {
        dictCategoryCode: 'CONTRACT_TERM_LV1'
      }

      let infoData = this.$refs.modlfrom.getData()
      console.log(infoData, '------------mainTag---------')
      if (infoData.mainTag) {
        reqData.dictName = infoData.mainTag
      }
      this.queryDictList(reqData, 'mainTag')
    },
    secTagSearch () {
      let reqData = {
        dictCategoryCode: 'CONTRACT_TERM_LV2'
      }
      let infoData = this.$refs.modlfrom.getData()
      if (infoData.secTag) {
        reqData.dictName = infoData.secTag
      }
      if (infoData.mainTag && this.list.mainTag.length > 0) {
        let itemDict = this._find(this.list.mainTag, (item) => {
          return infoData.mainTag === item.dictName
        })
        if (itemDict) {
          reqData.pDictCode = itemDict.dictCode
        }
      }
      this.queryDictList(reqData, 'secTag')
    },
    goBackList () {
      this.$router.push('/term/list')
    },
    // 插入要素事件
    insertElem (elem) {
      this.$refs.tinymceArea.insertElem(elem)
    },
    showFromModal () {
      this.$refs.tinymceArea.saveRecentContent()
      if (!this.content) {
        this.$Message.warning('请输入条款内容')
        return
      }
      this.$refs.modlfrom.tipShowChange(true)
      let reqData = {
        dictCategoryCode: 'CONTRACT_BUSINESS_TYPE',
        dictCode: '0'
      }
      this.$http.post('/dict/queryDictTreeList', reqData).then(res => {
        if (res.rtnCode === '000') {
          this.list.industry = this._isNil(res.data) ? [] : this.simplifyDictTreeList(res.data[0].childrenList)
          this.changeDatas('industry')
          if (this.id) { // 编辑赋值
            this.detailInfo.mainTag = this.detailInfo.mainTagName
            this.detailInfo.secTag = this.detailInfo.secTagName
            this.$refs.modlfrom.setData(this.detailInfo)
          }
        } else {
          this.$Message.error({content: res.rtnMsg, closable: true})
        }
        this.loading = false
      }).catch(err => {
        this.loading = false
        this.$debug(err)
      })
    },
    handleSubmit (infoData) {
      // 待后台告知内容字段的名称,暂用content表示条款内容字段,其他参数待填充
      let postData = {htmlContent: toFreeMaker(this.content, '', false)}
      let mainTagItem = this._find(this.list.mainTag, (item) => {
        return infoData.mainTag === item.dictName
      })
      let secTagItem = this._find(this.list.secTag, item => {
        return item.dictName === infoData.secTag
      })
      Object.assign(postData, infoData)
      postData.mainTag = mainTagItem.dictCode
      postData.secTag = secTagItem.dictCode
      let url = '/contractTerms/addContractTerm'
      if (this.id) {
        url = '/contractTerms/updateContractTerm'
        postData.id = this.id
      }
      this.$http.post(url, postData).then(res => {
        if (res.rtnCode === '000') {
          this.$router.push({name: 'TermList'})
        } else {
          this.$Message.error({ content: res.rtnMsg })
        }
      }).catch((err) => {
        this.$debug(err)
      })
    },

    // 编辑处理如下

    // 获取信息并展示
    showTermContent () {
      this.loading = true
      // 待后台告知接口名称,暂用todo
      this.$http.post('/contractTerms/getContractTermDetail', {id: this.id}).then(contentRes => {
        this.loading = false
        let content = this.getTermDetail(contentRes)
        // 初始化编辑器
        this.$refs.tinymceArea.init(defaultStyles)
        if (content) {
          // 获取内容中的要素
          let elems = pickElems(content)
          if (elems.length > 0) {
            this.loading = true
            // 若内容中有要素通过接口获取要素详情,此为已有接口
            this.$http.post('/contractElement/selectListByPage', {'idList': elems, 'noPage': '1'}).then(elemsRes => {
              this.loading = false
              this.reSetElems(elemsRes, content)
            }).catch(err => {
              this.loading = false
              this.$debug(err)
            })
          } else {
            this.content = content
          }
        }
      }).catch(err => {
        this.loading = false
        this.$debug(err)
      })
    },
    // 对接口获取到的条款详情进行赋值
    getTermDetail (res) {
      if (res.rtnCode === '000') {
        const resData = res.data || {}
        this.detailInfo = resData.contractTerms
        this.title = this.detailInfo.termName
        // resData.contractExamples
        this.$refs.exampleList.changeExampleList(resData.contractExamples)
        return resData.contractTerms.htmlContent || ''
      } else {
        this.$Message.error({content: res.rtnMsg, closable: true})
        return ''
      }
    },
    // 接口获取到的要素信息,替换当前内容里的要素变成编辑器要求展示的样子
    reSetElems (res, content) {
      if (res.rtnCode === '000') {
        const resData = res.data || []
        if (this._isNil(resData.rows) || resData.rows.length <= 0) {
          this.content = content
        } else {
          this.content = toEditor(resData.rows, content)
        }
      } else {
        this.$Message.error({content: res.rtnMsg})
      }
    }
  }
}
</script>

<style lang="scss" scoped>
.exampleP {
  font-size: 14px; 
  color: #242b39; 
  border-bottom: solid 1px #e3e3e3;
}
.example {
  width: 100%;
  border:1px solid #e1e3e9;
  min-height:70px;
  margin-top:15px;
  padding: 18px 14px; 

  &.on{ 
    border-color:#1e64e5;
  }
  .exampleP {
    font-size: 14px; 
    color: #242b39; 
    border-bottom: solid 1px #e3e3e3;
    overflow:hidden;
    text-overflow:ellipsis;
    white-space:nowrap;
    padding-left: 24px;
    background: url('../../assets/images/icon/text.png') no-repeat top left;
    cursor: pointer;
  }
  .exampleTitle {
    background: #e2f2fC;
    width: 45px;
    height: 22px;
    margin: -30px 0 10px -14px;
    text-align: center;
    line-height: 22px;
  }

  .exampleNode {
    padding:8px 0px;
    font-size: 12px;
    color: #292929;
    
    &::before {
      content: '章节';
      color: #999;
      position:absolute;
    }
    p {
      padding-left: 36px;
    }
  }
}
.panel-heading {
  margin: 0;
  padding: 17px 25px 17px 15px;

  .panel-title {
    display:inline-block;
    margin-left: 27px;
    vertical-align: middle;
    font-size: 14px;
    width: 800px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}
.demo-panel {
  display: inline-block;
  height: calc(100vh - 150px);
  width: calc(100% - 960px);
  overflow-y: auto;
  text-align: left;
  padding: 25px;
  margin-left: 10px;
  background: #fff;
}
.maxDiv {
  margin: 0 auto;
  width: 1280px;
}
.minDiv {
  margin: 0 auto;
  width: 960px;
}
</style
</style>

 

exampleList.vue  实战

主要功能  用于用户动态添加组件

代码如下

<template>
  <article>
    <modal-from ref="modlfrom" :title="title" 
    :label-width='labelWidth' 
    :width="width"
    :from-fileds="datas"
    :rules="rules"
    @handleSubmit="handleSubmit">
        <template slot="contractName">
          <p class="line">范例所处合约位置</p>  
        </template>
        <template slot="optionBtn">
            <p>
                <a v-if="nodeLength<3" @click="addNode">添加子章节</a>
                <a v-if="nodeLength>0" @click="delNode">删除</a>
            </p>
        </template>
    </modal-from>
    <div v-if="exampleList.length>0" >
        <article v-for="(exampleItem,index) in exampleList" :key='index'>
            <Poptip trigger="click" width="545" v-model="popTipShow[index]"  placement="left"   >
                <div class="api" slot="content">
                    <p style="color: #6c7078;">
                        范例内容 
                    <a class="ui-modal-close" @click="changePopTipshow(index)" style="top:2px"><i class="ui-icon ui-icon-ios-close-empty" style="font-size:16px"></i></a>
                    </p>
                    {{exampleItem.exampleContent}}
                </div>
                <div class="example"> 
                <div class="exampleTitle">范例{{ zhWList[index+1] }}</div>
                <p class="exampleP exampleText" :title='exampleItem.contractName'>
                    {{exampleItem.contractName}}
                </p>
                <div class='exampleNode '>
                    <p class="exampleText" :title='exampleItem.chapterOneName'> {{exampleItem.chapterOneName}}</p>
                    <p class="exampleText" :title='exampleItem.chapterTwoName'> {{exampleItem.chapterTwoName}}</p>
                    <p class="exampleText" :title='exampleItem.chapterThreeName'>{{exampleItem.chapterThreeName}}</p>
                    <p class="exampleText" :title='exampleItem.chapterFourName'> {{exampleItem.chapterFourName}}</p>
                    <div class="option">
                      <a class="del" @click.stop="delExample(exampleItem)"/>
                      <a class="edit" @click.stop="showFromModal(exampleItem)"/>
                    </div>
                </div>
            </div>
            </Poptip> 
        </article>
    </div>
   <tip-modal v-model="tipShow" title="删除" :onOk='deleteOption' >
      <template slot="content">
        <div class="content-text">
          <p >确定要删除该范例吗?</p>
        </div>
      </template>
    </tip-modal>
    <div class="example" style="height:176px;text-align:center;cursor: pointer;"  @click="showFromModal()" v-if="exampleList.length<3">
        <p style="font-size: 12px; color: #1e64e5; margin-top:58px;"> 
            <img src="@/assets/images/icon/add.png"> 添加范例
        </p>
    </div> 
  </article>
</template>

<script>
import ModalFrom from './../Modals/ModalForm'
import {exampleForms, lengthValidator} from './../Modals/termOption'
import TipModal from './../Modals/TipModal'
export default {
  name: 'term-example-page',

  components: {
    ModalFrom,
    TipModal
  },
  props: {
    termId: {
      type: String,
      required: true
    }

  },

  data () {
    return {
      title: '',
      zhWList: {
        1: '一',
        2: '二',
        3: '三',
        4: '四'
      },
      rules: {
        exampleContent: [
          { required: true, message: '请输入范例内容', trigger: 'change' },
          { validator: lengthValidator, trigger: 'change', maxLength: 1000, message: '范例内容不超过1000字' }
        ],
        contractName: [
          { required: true, message: '请输入合约名称', trigger: 'change' },
          { validator: lengthValidator, trigger: 'change', maxLength: 60, message: '合约名称不超过60字' }
        ],
        chapterOneName: [
          { validator: lengthValidator, trigger: 'change', maxLength: 20, message: '一级章节名称不超过20字' }
        ],
        chapterTwoName: [
          { validator: lengthValidator, trigger: 'change', maxLength: 20, message: '二级章节名称不超过20字' }
        ],
        chapterThreeName: [
          { validator: lengthValidator, trigger: 'change', maxLength: 20, message: '三级章节名称不超过20字' }
        ],
        chapterFourName: [
          { validator: lengthValidator, trigger: 'change', maxLength: 20, message: '四级章节名称不超过20字' }
        ]

      },
      id: '',
      tipShow: false,
      exampleList: [],
      detailInfo: {},
      labelWidth: 120,
      nodeLength: 2,
      width: 507,
      datas: [],
      popTipShow: {0: false, 1: false, 2: false} // 用数组简单的赋值v-modal绑定失效
    }
  },

  computed: {
  },
  watch: {
    nodeLength (newVal) {
      let i = 0
      this.datas = []
      this.datas = exampleForms().filter(item => {
        if (this.detailInfo) {
          item.value = this.detailInfo[item.model] || ''
        }
        if (item.isShow) {
          i++
          if (i <= newVal) {
            return true
          } else {
            return false
          }
        } else {
          return true
        }
      })
    }
  },
  mounted () {

  },

  beforeDestroy () {

  },

  methods: {
    changePopTipshow (index) {
      this.popTipShow[index] = false
    },
    changeExampleList (list) {
      this.exampleList = list
    },
    addNode () {
      if (this.nodeLength < 4) { this.nodeLength++ }
    },
    delNode () {
      if (this.nodeLength > 0) { this.nodeLength-- }
    },
    showFromModal (detailInfo) {
      this.detailInfo = detailInfo
      this.title = '添加范例'
      if (detailInfo) {
        this.title = '编辑范例'
      }
      this.$refs.modlfrom.tipShowChange(true)
      let i = 0
      this.datas = exampleForms().filter(item => {
        if (detailInfo) {
          item.value = detailInfo[item.model] || ''
          this.id = detailInfo.id
        }
        if (item.isShow) {
          i++
          if (i <= this.nodeLength) {
            return true
          } else {
            return false
          }
        } else {
          return true
        }
      })
      if (detailInfo && detailInfo.chapterFourName) {
        this.nodeLength = 3
      } else {
        this.nodeLength = 2
      }
    },
    delExample (detailInfo) { // 删除
      this.detailInfo = detailInfo
      this.tipShow = true
    },
    deleteOption () {
      this.$http.post('/contractTerms/deleteTermExample', {id: this.detailInfo.id}).then(res => {
        if (res.rtnCode === '000') {
          let selecItem = this.exampleList.find((item, index) => {
            item.index = index
            return item.id === this.detailInfo.id
          })
          this.exampleList.splice(selecItem.index, 1)
          this.$Message.success('删除成功')
          this.tipShow = false
        } else {
          this.$Message.error({ content: res.rtnMsg })
        }
      }).catch((err) => {
        this.$debug(err)
      })
    },
    handleSubmit (infoData) {
      // 待后台告知内容字段的名称,暂用content表示条款内容字段,其他参数待填充
      let postData = {termId: this.termId}

      Object.assign(postData, infoData)
      let url = '/contractTerms/addTermExample'
      if (this.id) {
        url = '/contractTerms/updateTermExample'
        postData.id = this.id
      }
      this.$http.post(url, postData).then(res => {
        if (res.rtnCode === '000') {
          this.$refs.modlfrom.tipShowChange(false)
          if (this.id) {
            let selecItem = this.exampleList.find((item, index) => {
              item.index = index
              return item.id === this.id
            })
            this.exampleList.splice(selecItem.index, 1, postData)
            this.id = ''
            this.$Message.success('编辑范例成功')
          } else {
            postData.id = res.data
            this.exampleList.push(postData)
            this.$Message.success('添加范例成功')
          }
        } else {
          this.$Message.error({ content: res.rtnMsg })
        }
      }).catch((err) => {
        this.$debug(err)
      })
    }
  }

}
</script>

<style lang="scss" scoped>
.api {
    min-height: 100px;
    color: #292929;
    white-space: initial;
    max-height: 250px;
}
p.line {
    border-top: 1px solid #e3e3e3;
    display: inline-block;
    position: absolute;
    width: 450px;
    left: -120px;
    margin-top: 22px;
    padding-left: 22px;
    color: #C2C5BC;
    padding-top: 5px
}
.exampleP {
  font-size: 14px; 
  color: #242b39; 
  border-bottom: solid 1px #e3e3e3;
}
.example {
  width: 278px;
  display: inline-block;
  border:1px solid #e1e3e9;
  position: relative;
  min-height:70px;
  margin-top:15px;
  padding: 18px 14px; 

  &.on{ 
    border-color:#1e64e5;
  }
  .exampleText {
    overflow:hidden;
    text-overflow:ellipsis;
    white-space:nowrap;
    cursor: pointer
  }

  .exampleP {
    font-size: 14px; 
    color: #242b39; 
    border-bottom: solid 1px #e3e3e3;
    padding-left: 24px;
    background: url('../../../assets/images/icon/text.png') no-repeat top left 
  }
  .exampleTitle {
    position: absolute;
    top: -11px;
    background: #E2F2FC;
    width: 45px;
    height: 22px;
    left: 0;
    text-align: center;
    line-height: 22px;
  }

  .exampleNode {
    padding:8px 0px;
    font-size: 12px;
    color: #292929;
    
    &::before {
      content: '章节';
      color: #999;
      position:absolute;
    }

    .option {
      margin-top: 8px;
      text-align: right;
      
      a {
        background-repeat: no-repeat;
        background-position: center center;
        width: 16px;
        height: 16px;
        float: right;
        margin-right: 8px;
      }

      .edit {
         background-image: url('../../../assets/images/icon/edit.png');

         &:hover {
           background-image: url('../../../assets/images/icon/editOn.png');
         }
      }
      .del {
         background-image: url('../../../assets/images/icon/del.png');

         &:hover {
           background-image: url('../../../assets/images/icon/delOn.png');
         }
      }
    }

    p {
      padding-left: 36px;
    }
  }
}

</style>

 

实战核心配置js  

termOption.js     注意: 这个是核心,日常维护只需要处理这个配置文件,就可以解决大部分问题。 方便维护字段维护和添加

// import { formatDate } from '@util/tool'

// const mobileValid = (rule, val, cb) => {
//   let checkPhone = (mobile) => {
//     let tel = /^0\d{2,3}-?\d{7,8}$/
//     let phone = /^1[34578]\d{9}$/
//     if (tel.test(mobile) || phone.test(mobile)) {
//       return false
//     }
//     return true
//   }
//   // 指定截止日期 时做空验证
//   if (checkPhone(val)) {
//     cb(new Error('请输入格式正确的固话和手机号'))
//   }
//   cb()
// }

const termName = (rule, val, cb) => {
  if (val.length > 100) {
    cb(new Error('条款名称不超过100字'))
  }
  cb()
}

export const lengthValidator = (rule, val, cb) => {
  if (val.length > rule.maxLength) {
    cb(new Error(rule.message))
  }
  cb()
}

// 数字框的处理保留
const moneyValidChange = (rule, val, cb) => {
  // eslint-disable-next-line no-useless-escape
  val = val.replace(/\,/g, '').replace('-', '')
  if (this._isEmpty(val)) {
    cb(new Error('请填写合同金额'))
  } else if (this.isNumberInt(val)) {
    cb(new Error('请输入正确数字'))
  }
  cb()
}

const applyText = (rule, val, cb) => {
  if (val && val.length > 100) {
    cb(new Error(rule.name + '不超过100字'))
  }
  cb()
}
const termKeyWord = (rule, val, cb) => {
  if (val && val.length > 30) {
    cb(new Error('不超过30字,关键字之间以逗号间隔'))
  }
  cb()
}

const moneyValidBlur = (rule, val, cb) => {
  // eslint-disable-next-line no-useless-escape
  val = val.replace(/\,/g, '').replace('-', '')
  if (this._isEmpty(val)) {
    cb(new Error('请填写合同金额'))
  } else if (this.isNumberInt(val)) {
    cb(new Error('请输入正确数字'))
  } else {
    this.signInfoData[rule.field] = parseFloat(val).toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,')
  }
  cb()
}

export const signRules = {
  money: [
    { validator: moneyValidChange, trigger: 'change' },
    { validator: moneyValidBlur, trigger: 'blur' }
  ],
  termName: [
    { required: true, message: '请输入条款名称', trigger: 'change' },
    { validator: termName, trigger: 'change' }
  ],
  industry: [
    { required: true, message: '请选择行业', trigger: 'change' }
  ],
  l1Class: [
    { required: true, message: '请选择业务类型', trigger: 'change' }
  ],
  l2Class: [
    { required: true, message: '请选择产品类型', trigger: 'change' }
  ],
  l3Class: [
    { required: true, message: '请选择产品细分', trigger: 'change' }
  ],
  applyObject: [
    { validator: applyText, trigger: 'change', name: '适用对象' }
  ],
  applyScence: [
    { validator: applyText, trigger: 'change', name: '适用场景' }
  ],
  termKeyword: [
    { validator: termKeyWord, trigger: 'change' }
  ],
  remark: [
    { validator: applyText, trigger: 'change', name: '备注' }
  ]

}

export const addProjectForms = () => {
  return [
    {
      type: 'input',
      label: '条款名称:',
      model: 'termName',
      prop: 'termName',
      rules: signRules.termName,
      placeholder: '条款名称不超过100字'
    },
    {
      type: 'select',
      label: '行业:',
      model: 'industry',
      prop: 'industry',
      showModel: 'industryName',
      rules: signRules.industry,
      placeholder: '',
      children: []
    },
    {
      type: 'select',
      label: '业务类型:',
      model: 'l1Class',
      prop: 'l1Class',
      showModel: 'l1ClassName',
      rules: signRules.l1Class,
      placeholder: ''
    },
    {
      type: 'select',
      label: '产品类型:',
      model: 'l2Class',
      prop: 'l2Class',
      showModel: 'l2ClassName',
      rules: signRules.l2Class,
      placeholder: '',
      children: []
    },
    {
      type: 'select',
      label: '产品细分:',
      model: 'l3Class',
      prop: 'l3Class',
      showModel: 'l3ClassName',
      rules: signRules.l3Class,
      placeholder: '',
      children: []
    },
    {
      type: 'autoComp',
      label: '条款主标签:',
      model: 'mainTag',
      prop: 'mainTag',
      showModel: 'mainTagName',
      eventName: 'mainTagEvent',
      placeholder: '',
      children: []
    },
    {
      type: 'autoComp',
      label: '条款次标签:',
      model: 'secTag',
      prop: 'secTag',
      showModel: 'secTagName',
      eventName: 'secTagEvent',
      placeholder: '',
      children: []
    },
    {
      type: 'input',
      label: '条款关键字:',
      model: 'termKeyword',
      rules: signRules.termKeyword,
      prop: 'termKeyword',
      placeholder: '不超过30字。例:银行同业,货币基金',
      children: []
    },
    {
      type: 'textarea',
      label: '适用对象:',
      model: 'applyObject',
      prop: 'applyObject',
      rules: signRules.applyObject,
      class: 'middleSize',
      placeholder: '100个字内',
      children: []
    },
    {
      type: 'textarea',
      label: '适用场景:',
      model: 'applyScence',
      prop: 'applyScence',
      rules: signRules.applyScence,
      class: 'middleSize',
      placeholder: '100个字内',
      children: []
    },
    {
      type: 'textarea',
      label: '备注:',
      model: 'remark',
      prop: 'remark',
      rules: signRules.remark,
      class: 'middleSize',
      placeholder: '100个字内',
      children: []
    }

  ]
}

// 范例字段
export const exampleForms = () => {
  return [
    {
      type: 'inputSlot',
      label: '所属合约名称',
      model: 'contractName',
      prop: 'contractName',
      slot: 'contractName',
      class: 'contractName',
      placeholder: '所属合约名称不超过60字'
    },
    {
      type: 'input',
      label: '一级章节名称 ',
      model: 'chapterOneName',
      prop: 'chapterOneName',
      placeholder: '例:"第一章增级对象“'
    },
    {
      type: 'input',
      label: '二级章节名称 ',
      model: 'chapterTwoName',
      prop: 'chapterTwoName',
      isShow: true,
      placeholder: '例:"1.1内部增级“'
    },
    {
      type: 'input',
      label: '三级章节名称 ',
      model: 'chapterThreeName',
      prop: 'chapterThreeName',
      isShow: true,
      placeholder: '例:"4.1.1内部增级的具体策略"'
    },
    {
      type: 'input',
      label: '四级章节名称 ',
      model: 'chapterFourName',
      prop: 'chapterFourName',
      isShow: true,
      placeholder: '例“4.1.1.1内部增级的方式”'
    },
    {
      type: 'text',
      slot: 'optionBtn',
      prop: 'optionBtn',
      class: 'optionBtn'
    },
    {
      type: 'textarea',
      label: '范例内容',
      model: 'exampleContent',
      prop: 'exampleContent',
      class: 'lineMiddleSize',
      placeholder: '1000个字内'
    }
  ]
}

 

 类似资料: