vue 表单封装(利用iview自带组件)
实战代码功能如下1.inputNumber 数字框 千分位和小数点 处理。(详细功能会另起文章)2.AutoComplete 和select 框的联动处理3. 弹框功能和弹框表单4.动态添加表单字段5.实战富文本编辑器另起文档说明。封装目的和解决痛点,表单字段 10几个20个时候,需要人为的不断拷贝和添加 相同的代码 特别是iview 冗余代码会非常多,ht...
实战代码功能如下
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个字内'
}
]
}
更多推荐
所有评论(0)