实战代码功能如下
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个字内'
}
]
}