该组件是一个动态生成表单的Vue组件,通过传入formList参数和插槽的方式实现表单项的定制化渲染
组件的封装
HCommonFormItem
根据传入的"formList"参数生成一组表单项,并通过插槽(slot)将表单项渲染出来。
- 在components下创建commonFormItem文件夹,在该文件夹下创建index.vue文件
<template>
<div class="block-message-box">
<template v-for="(item, index) in formList">
<template v-if="item.display===undefined || item.display===true">
<HCommonBlock :title="item.title" :key="index" class="form-box" :hasBorder="item.hasBorder">
<template slot="title-button" v-if="item.slot">
<slot :name="'title-button-'+ item.key"></slot>
</template>
<h-common-form
@label-click="handleLabelClick"
:hideRequiredAsterisk="hideRequiredAsterisk"
:hasLabelImg="hasLabelImg"
:ref="'myForm' + index + item.key"
:formRules="item.formValidate"
:formData="item.purchasingInformation"
:fileList="item.fileList"
@upload="handleImgInfo"
@uploadLoad="handleImgLoad"
@check="handleChecked"
@detail="handleLookDetail"
@addTableRow="handleAddTableRow"
@tableEvent="handleTableEvent"
@tableSelect="handleSelectTable"
@address="handleSelectAddress"
@operation="handleOperation"
@input-blur="handleInputBlur"
@select-change="handleSelectChange"
@radio="handleRadioGroupChange"
@querySearch="handleQuerySearch"
@remote-method="handleRemoteMethod"
@autocomplete-change="handleSelectAutocomplete"
>
<template v-for="item in slotNameList" >
<div :slot="item" :key="item">
<slot :name="item" ></slot>
</div>
</template>
</h-common-form>
</HCommonBlock>
</template>
</template>
<slot></slot>
</div>
</template>
<script>
import HCommonForm from '@/components/commonForm'
import HCommonBlock from '@/components/commonBlock'
export default {
name: 'HCommonFormItem',
props: {
formList: {
type: Array,
default() {
return []
}
},
slotNameList:{
type: Array,
default() {
return []
}
},
hasLabelImg: {
type: Boolean,
default: true
},
hideRequiredAsterisk: {
type: Boolean,
default: false
},
},
components: {
HCommonForm,
HCommonBlock
},
methods: {
handleLabelClick(info,val) {
this.$emit('label-click',info,val)
},
handleOperation (operationType, info, column) {
this.$emit('operation', operationType, info, column)
},
handleInputBlur(val, field, parentField) {
this.$emit('input-blur', val, field, parentField)
},
handleSelectChange (val, field, parentField,info) {
this.$emit('select-change', val, field, parentField,info)
},
handleImgInfo (val, file, info) {
this.$emit('upload', val, file, info)
},
handleImgLoad (val, file, info) {
this.$emit('uploadLoad', val, file, info)
},
handleChecked (value, currtentVal) {
this.$emit('check', value, currtentVal)
},
handleLookDetail (currtentVal) {
this.$emit('detail', currtentVal)
},
handleAddTableRow (currtentVal) {
this.$emit('addTableRow', currtentVal)
},
handleTableEvent (event, $index, info) {
this.$emit('tableEvent', event, $index, info)
},
handleSelectTable (currentVal, currentLabel, column, $index, info) {
this.$emit('tableSelect', currentVal, currentLabel, column, $index, info)
},
handleSelectAddress (val, label, row,districtLevel) {
this.$emit('address', val, label, row,districtLevel)
},
handleRadioGroupChange (val, row) {
this.$emit('radio', val, row)
},
handleQuerySearch(queryString, cb, info) {
this.$emit('querySearch',queryString, cb, info)
},
handleSelectAutocomplete(val, field, parentField, info) {
this.$emit('autocomplete-change', val, field, parentField, info)
},
handleRemoteMethod(val, field, parentField, info) {
this.$emit("remote-method", val, field, parentField, info);
},
}
}
</script>
<style lang="scss" scoped>
.block-message-box {
height: fit-content;
width: 100%;
}
</style>
- 组件中使用了两个子组件:
- HCommonForm: 这是一个自定义的表单组件,用于展示和处理表单数据。
- HCommonBlock: 这是一个自定义的块级元素组件,用于包裹表单项,并提供标题和边框等样式。
组件的模板部分使用了Vue的指令和循环语法,通过v-for指令遍历formList数组,生成多个HcommonBlock组件。每个HcommonBlock组件中包含了一个HCommonForm组件,根据item对象的属性设置HCommonForm组件的props。
HCommonBlock
该组件是实现一个可定制样式的块级容器组件,可以根据需要显示标题和内容,并支持选择不同的选项卡。
- 在components下创建HCommonBlock文件夹,在该文件夹下创建index.vue文件
<template>
<div
class="block-box"
:class="[
noPadding ? 'block-box-no-padding' : '',
hasBorder ? 'block-box-border' : '',
noMargin ? 'block-box-no-margin' : ''
]"
>
<div class="block-title" v-if="title">
{{ title }}
</div>
<div v-else-if="blockTitleList.length > 0" class="block-title-box">
<div
:class="
chooseTab == index ? 'block-title' : 'block-title block-title-no-bg '
"
v-for="(item, index) in blockTitleList"
:key="index"
@click="handleChooseTab(item, index)"
>
{{ item.name }}
</div>
</div>
<slot name="title-button"></slot>
<div
class="block-content-box"
:class="[
noOverFlow ? 'block-content-box-overflow-no' : '',
isScroll ? 'common-scroll' : ''
]"
>
<slot></slot>
</div>
</div>
</template>
<script>
export default {
data () {
return {
chooseTab: 0
}
},
props: {
title: String,
noPadding: {
type: Boolean,
default: false
},
hasBorder: {
type: Boolean,
default: false
},
noMargin: {
type: Boolean,
default: false
},
blockTitleList: {
type: Array,
default () {
return []
}
},
noOverFlow: {
type: Boolean,
default: false
},
isScroll: {
type: Boolean,
default: false
}
},
methods: {
handleChooseTab (val, index) {
if (this.chooseTab == index) return
this.chooseTab = index
this.$emit('choose', val)
}
},
components: {}
}
</script>
<style lang="scss" scoped>
.block-box {
width: calc(100% - 3.54167vw);
height: fit-content;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 60px;
position: relative;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 60px;
}
&.block-box-no-padding {
.block-content-box {
padding: 0;
background: transparent;
}
}
&.block-box-no-margin {
margin-bottom: 0;
}
&.block-box-border {
.block-content-box {
border-radius: 10px;
border: 1px solid #000000;
background: transparent;
overflow: hidden;
&-overflow-no {
overflow: inherit;
}
&.common-scroll {
overflow-y: auto;
}
}
}
.block-title-box {
display: flex;
.block-title {
cursor: pointer;
border-radius: 0;
&:first-child {
border-radius: 20px 0 0px 0px;
}
&:last-child {
border-radius: 0 20px 0px 0px;
}
}
}
.block-title {
min-width: 360px;
width: fit-content;
height: 65px;
background: #808c87;
border-radius: 20px 20px 0px 0px;
font-size: 32px;
font-weight: 600;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
&.block-title-no-bg {
background: #dee2e0;
}
}
.block-content-box {
min-height: 80px;
height: fit-content;
background: #ffffff;
border-radius: 10px;
width: 100%;
padding: 40px 40px 35px 40px;
box-sizing: border-box;
}
}
</style>
- 模板中的
是整个容器的最外层元素,它使用flex布局,并且垂直居中对齐。它可以根据传入的props动态添加不同的类名,从而实现样式的变化。
- 在
中会显示标题,如果没有传入标题,则会根据blockTitleList数组中的数据渲染多个标题选项,点击选项会触发handleChooseTab方法,并通过$emit将选中的选项值传递给父组件。
-
用于显示内容,可以根据传入的props进行样式控制,如是否有边框、是否可滚动等。
HCommonForm
该组件实现了一个灵活配置的表单组件,可以根据传入的数据动态生成表单项,并支持各种类型的输入框、下拉框、日期选择等功能。同时也包含了上传文件、显示图片、查看PDF等功能。
- 在components下创建HCommonForm文件夹,在该文件夹下创建index.vue文件
<template>
<div
class="h-common-form"
>
<el-form
ref="rulesForm"
:inline="false"
:model="formData.form"
label-position="right"
:rules="formRules"
:hide-required-asterisk="hideRequiredAsterisk"
:class="[
!hideRequiredAsterisk&&hasLabelImg?'form-label-Required':'',
!hideRequiredAsterisk && !hasLabelImg ? 'form-label-Required-no-image' : ''
]"
>
<el-form-item
v-for="(info, formI) in formData.formLabel"
:key="info.field + formI"
:prop="info.field"
:style="[
{
width: !info.widthIsNotNum
? getWidth(info.itemWidth)
: info.itemWidth
},
{
marginRight: !info.widthIsNotNum
? getWidth(info.marginRight)
: info.marginRight
}
]"
v-show="info.display"
:rules="info.display ? formRules[info.field] : [{ required: false }]"
:class="[
{ 'common-form-item-font-weight': info.wordBold },
{ 'common-form-item-font-color': info.wordColor },
{ 'common-form-item-font-color-red': info.wordColorRed },
{ 'common-form-item-algin-right': info.align == 'right' },
{ 'common-form-item-algin-center': info.align == 'center' },
{ 'common-form-item-margin-bottom': info.marginBottom },
{ 'common-form-item-margin-bottom-middle': info.marginBottomMiddle },
{ 'common-form-item-content-width-fit': info.widthFit },
{
'common-form-item-content-height':
(info.type == 'textarea' && !info.isSpecial) ||
info.type == 'upload'
},
{
'common-form-item-content-height-no':
info.type == 'textarea' && info.isSpecial
},
{ 'common-form-item-no-label': info.noLabel },
{
'common-form-select-placeholder-right':
info.selectPlaceholderAlign == 'right'
},
{
'common-form-disabled-label-algin-right':
info.disabledAlign == 'right'
},
{
'common-form-disabled-label-algin-center':
info.disabledAlign == 'center'
},
{
'common-form-input-placeholder-right':
info.inputPlaceHolderAlign == 'right'
},
{ 'common-form-input-label-right': info.inputAlign == 'right' },
{ 'common-form-input-label-center': info.inputAlign == 'center' },
{ 'common-form-item-border-line': info.type == 'border' },
{ 'common-form-item-disabled': info.disabled }
]"
>
<template v-if="info.isUseSlot">
<div slot="label" class="common-form-label">
<img
:style="[
{ width: info.iconWidth ? getWidth(info.iconWidth) : '' },
{
marginRight: info.iconMarginRight
? getWidth(info.iconMarginRight)
: ''
},
{ height: info.iconHeight ? getWidth(info.iconHeight) : '' }
]"
:src="require(`@/assets/harley/${info.icon}.png`)"
v-if="info.icon"
/>
</div>
<slot :name="info.field"></slot>
</template>
<template v-else>
<span slot="label" class="common-form-label">
<img
:style="[
{ width: info.iconWidth ? getWidth(info.iconWidth) : '' },
{
marginRight: info.iconMarginRight
? getWidth(info.iconMarginRight)
: ''
},
{ height: info.iconHeight ? getWidth(info.iconHeight) : '' }
]"
:src="require(`@/assets/harley/${info.icon}.png`)"
v-if="info.icon"
/>
<span>{{ info.label ? info.label + ":" : "" }}</span>
</span>
<div v-if="info.type == 'border'" class="common-form-underLine"></div>
<div
v-if="
info.disabled &&
judgeTypeVisi(info.type) &&
info.dataType != 'multiple'
"
class="common-form-content-label"
:class="[
{
'common-form-content-label-block-bg-normal': info.isBlockBg
},
{
'common-form-content-label-block-bg-abnormal':
info.isBlockBg && Number(formData.form[info.field]) <= 0
}
]"
>
<common-tooltip
:twoLine="!(info.widthIsNotNum && info.itemWidth == '100%')"
:disabled="false"
:isClick="info.isClick"
:twoRow="
info.widthIsNotNum &&
info.itemWidth == '100%' &&
info.type == 'textarea'
"
:className="'ellipsisName' + info.field"
:refName="'toolitipName' + info.field"
:content="formData.form[info.field].toString()"
:isCopy="info.isCopy"
@label-click="handleLabelClick(info,formData.form[info.field])"
>
</common-tooltip>
<span
v-if="
info.unit &&
formData.form[info.field] !== '' &&
formData.form[info.field] !== undefined
"
class="unit"
>{{ info.unit }}</span
>
</div>
<el-input
v-if="
(info.type === 'text' ||
info.type === 'password' ||
info.type === 'textarea') &&
!info.disabled &&
info.dataType != 'multiple'
"
:type="info.type"
:placeholder="info.placeholder || ''"
:disabled="info.disabled"
v-model.trim="formData.form[info.field]"
@blur="
handleInputBlur(
formData.form[info.field],
info.field,
info.parentField
)
"
><i
v-show="
formData.form[info.field] &&
formData.form[info.field].length > 0 &&
info.passwordType == 'password'
"
slot="suffix"
:class="[
info.type == 'password' ? 'el-icon-minus' : 'el-icon-view'
]"
autocomplete="auto"
@click="$emit('passwordClick', info)"
/>
<template slot="append" v-if="info.unit">{{ info.unit }}</template>
</el-input>
<div
class="is-multiple-textarea"
v-if="info.type === 'textarea' && info.dataType == 'multiple'"
>
<div
v-for="(textareaItem, textareaIndex) in formData.form[info.field]"
:key="textareaIndex + 'textarea'"
class="textarea-item"
>
<el-input
:type="info.type"
:autosize="{ minRows: 2, maxRows: 10 }"
:placeholder="info.placeholder || ''"
:disabled="formData.form[info.field][textareaIndex].disabled || info.disabled"
v-model="formData.form[info.field][textareaIndex].value"
>
</el-input>
</div>
<div
v-if="info.dataType == 'multiple' && !info.disabled"
class="add-remark"
@click="handleAddRemark(formData.form[info.field])"
>
<img src="@/assets/harley/add.png" alt="" />
</div>
</div>
<el-autocomplete
v-if="info.type === 'autocomplete'"
class="inline-input"
:fetch-suggestions="
(queryString, cb) => handleQuerySearch(queryString, cb, info)
"
:value-key="info.prop.value"
:placeholder="info.placeholder || ''"
:disabled="info.disabled"
v-model.trim="formData.form[info.field]"
@select="
handleSelectAutocomplete(
$event,
info.field,
info.parentField,
info
)
"
@change="
handleSelectAutocomplete(
$event,
info.field,
info.parentField,
info
)
"
:clearable="info.clearable"
:popper-class="
$store.getters.windowWidth <= 1024
? 'el-autocomplete-suggestion-width'
: ''
"
>
<template slot-scope="{ item }">
<div class="name">{{ item[info.prop.label] }}</div>
</template>
</el-autocomplete>
<h-date-picker
v-if="info.type === 'date'"
v-model="formData.form[info.field]"
:clearable="info.clearable"
:start-placeholder="
info.startPlaceholder ? info.startPlaceholder : ''
"
:end-placeholder="info.endPlaceholder ? info.endPlaceholder : ''"
:placeholder="info.placeholder || ''"
:type="info.dateType"
:value-format="info.valueFormat"
:prefix-icon="'el-icon-caret-bottom'"
:disabled="info.disabled"
:keyValue="info.field + info.dateType"
:picker-options="info.pickerOptions"
></h-date-picker>
<h-select
v-if="info.type === 'select' && !info.multiple"
v-model="formData.form[info.field]"
:ref="info.field"
:data-source="info.options"
:clearable="info.clearable === undefined ? true : info.clearable"
:align="info.align"
:collapseTags="info.collapseTags"
:placeholder="info.placeholder || ''"
:disabled="info.disabled"
:allowCreate="info.allowCreate"
:props="info.props"
:remote="info.remote"
:reserve-keyword="info.reserveKeyword"
:remote-method="
$event =>
handleRemoteMethod($event, info.field, info.parentField, info)
"
:multiple="info.multiple"
:filterable="info.filterable"
@change="
handleSelectChange($event, info.field, info.parentField, info)
"
:selectSpecial="info.selectSpecial"
>
<template slot="option-content" v-if="info.multiple">
<el-checkbox-group v-model="formData.form[info.field]">
<el-option
v-for="(item, index) in info.options"
:key="index"
:label="
info.prop && info.prop.label
? item[info.prop.label]
: item.name
"
:value="
info.prop && info.prop.value
? item[info.prop.value]
: item.id
"
>
<el-checkbox
style="pointer-events: none"
:label="
info.prop && info.prop.value
? item[info.prop.value]
: item.id
"
>{{
info.prop && info.prop.label
? item[info.prop.label]
: item.name
}}
</el-checkbox>
</el-option>
</el-checkbox-group>
</template>
</h-select>
<HSelectMultiple
v-if="info.type === 'select' && info.multiple"
:select-info="info"
v-model="formData.form[info.field]"
:filter-val="info.filterVal"
@input-search="dropDownSearchTop($event, info)"
@change="changeSelect($event, info.field, info.parentField, info)"
>
</HSelectMultiple>
<address-cascader
v-if="info.type === 'district'"
:selectedVal="formData.form[info.field]"
@getVal="
(districtVal, districtLabel, districtLevel) =>
setDistrictId(districtVal, districtLabel, info, districtLevel)
"
:isNeedFilter="info.isNeedFilter"
:checkStrictly="info.checkStrictly"
ref="addressCascader"
:disabled="info.disabled"
></address-cascader>
<h-radio-group
v-if="info.type === 'radio' && info.display"
:value="formData.form[info.field]"
:disabled="info.disabled"
:data-source="info.options"
:props="info.props"
@change="val => handleRadioGroupChange(val, info)"
>
</h-radio-group>
<div
v-if="info.type === 'upload'"
class="upload-box"
>
<el-upload
:ref="info.isDynamic ? 'upload' + info.field : 'upload'"
:before-upload="
e => {
beforeAvatarUpload(e, info);
}
"
action="#"
class="file-upload-box"
:disabled="info.disabled"
:multiple="info.multiple"
:limit="info.limit"
:headers="header"
:on-exceed="
e => {
handleExceed(e, info.limit, info);
}
"
:http-request="
(e, file) => {
handleAvatarSuccess(e, file, info);
}
"
:file-list="info.isDynamic ? info.fileList : fileList"
>
<el-button
:disabled="info.disabled"
class="specailButton"
:loading="imgLoading.indexOf(info.field) != -1"
type="info"
>上传
</el-button>
<template slot="file" slot-scope="{ file }">
<div class="upload-file-name-box">
<div class="file-icon">
<img src="@/assets/harley/uploadImgIcon.png" />
</div>
<div class="file-info">
<span class="el-upload-list__item-name">{{
file.name
}}</span>
<div class="file-btn-box">
<el-button
class="file-btn danger"
@click="handleRemove(file, info)"
v-if="!info.disabled"
>
删除
</el-button>
<el-button
class="file-btn"
@click="handleDownLoadEdit(file, info)"
v-if="file.url"
:loading="
downLoading.indexOf(file.code) != -1 ||
downLoading.indexOf(file.uid) != -1
"
>
下载
</el-button>
<el-button
class="file-btn"
@click="handleEditPreview(file, info)"
v-if="commonFunc.judgeImg(file.name) && file.url"
:loading="
canEditImgLoading.indexOf(file.code) != -1 ||
canEditImgLoading.indexOf(file.uid) != -1
"
>
查看
</el-button>
<el-button
class="file-btn"
@click="handleEditPreviewPdf(file, info)"
v-if="commonFunc.judgePdf(file.name) && file.url"
:loading="
openPdfEditLoading.indexOf(file.uid) != -1 ||
openPdfEditLoading.indexOf(file.code) != -1
"
>
查看
</el-button>
</div>
<viewer :images="formImgData" ref="viewer" v-show="false">
<img
:src="itemImg.url"
:alt="itemImg.name ? itemImg.name : ''"
v-for="(itemImg, indexi) in formImgData"
:key="indexi"
:class="
itemImg.code
? 'viewerImg' + itemImg.code + info.field
: 'viewerImg' + itemImg.uid + info.field
"
/>
</viewer>
</div>
</div>
</template>
</el-upload>
<el-popover
placement="right"
style="display: inline-block; margin-left: 0"
v-if="info.question"
:popper-class="
$store.getters.windowWidth <= 1024
? info.question && info.question.image
? 'popover-toolip popover-toolip-wdith popover-toolip-upload-width'
: 'popover-toolip popover-toolip-wdith'
: info.question && info.question.image
? 'popover-toolip popover-toolip-upload'
: 'popover-toolip'
"
ref="popover"
trigger="hover"
@click.stop="() => {}"
>
<span v-if="info.question.text">{{ info.question.text }}</span>
<img
:src="info.question.image"
alt=""
v-if="info.question.image"
:style="{ width: getWidth(info.question.width) }"
/>
<i class="el-icon-question mgl10 pointer" slot="reference"></i>
</el-popover>
</div>
</template>
</el-form-item>
<slot></slot>
</el-form>
</div>
</template>
<script>
import commonTooltip from "@/components/commonTooltip/index";
import HSelectMultiple from "@/common-ui/selectMultiple/index";
import HSelect from "@/common-ui/select/index";
import HDatePicker from "@/common-ui/datePicker";
import addressCascader from "@/components/addressCascader/index";
import { getToken } from "@/utils/auth";
import HRadioGroup from "@/common-ui/radioGroup";
import { uploadImg } from "@/api/district";
import { axiosGet } from "@/api/axios";
import { validateFileName } from "@/utils/validate";
import { downLoadFile } from "@/utils/download";
export default {
components: {
HSelect,
HDatePicker,
addressCascader,
commonTooltip,
HRadioGroup,
HSelectMultiple
},
props: {
formData: {
type: Object,
default() {
return {};
}
},
formRules: {
type: Object,
default() {
return {};
}
},
labelWidth: {
type: String,
default() {
return "80px";
}
},
uploadImg: {
type: Boolean,
default() {
return true;
}
},
disabled: {
type: Boolean,
default: false
},
hideRequiredAsterisk: {
type: Boolean,
default: false
},
hasLabelImg: {
type: Boolean,
default: true
},
fileList: {
type: Array,
default: () => {
return [];
}
}
},
data() {
return {
imgLoading: [],
openPdfEditLoading: [],
canEditImgLoading: [],
downLoading: [],
imgUploadList: [],
formImgData: [],
checkText: "",
startVal: ""
};
},
watch: {},
computed: {
header() {
return {
Authorization: "YWTE#" + getToken(),
"Content-Type": "multipart/form-data",
AuthSysCode: "RSPV"
};
}
},
beforeDestroy() {},
methods: {
handleLabelClick(info,val) {
this.$emit('label-click',info,val)
},
handleQuerySearch(queryString, cb, info) {
if (queryString == "") {
cb([]);
} else {
this.$emit("querySearch", queryString, cb, info);
}
},
handleSelectAutocomplete(val, field, parentField, info) {
this.$emit("autocomplete-change", val, field, parentField, info);
},
handleAddRemark(item) {
item.push({
date: "",
userName: "",
disabled: false,
value: ""
});
},
changeSelect(val, field, parentField, info) {
console.log(val, "kkkkk");
info.chooseSelectList = [];
for (let i = 0; i < info.filterOptions.length; i++) {
for (let j = 0; j < val.length; j++) {
let value = info.prop && info.prop.value ? info.prop.value : "id";
if (val[j] === info.filterOptions[i][value]) {
info.chooseSelectList.push(info.filterOptions[i]);
}
}
}
this.$emit("change", val, field, parentField, this.parentIndex);
},
dropDownSearchTop(val, info) {
info.filterVal = val;
if (info.filterVal === "") {
info.options = JSON.parse(JSON.stringify(info.filterOptions));
return;
}
let list = [];
if (info.chooseSelectList.length > 0) {
list = info.filterOptions.filter(item => {
let value = info.prop && info.prop.value ? info.prop.value : "id";
return info.chooseSelectList.every(el => el[value] != item[value]);
});
} else {
list = JSON.parse(JSON.stringify(info.filterOptions));
}
info.options = info.chooseSelectList.concat(
list.filter(item => {
let name = info.prop && info.prop.label ? info.prop.label : "name";
return item[name].includes(info.filterVal);
})
);
},
handleRadioGroupChange(val, row) {
this.$emit("radio", val, row);
},
judgeTypeVisi(type) {
let typeList = ["text", "textarea"];
return typeList.indexOf(type) != -1;
},
getWidth(val, column, row) {
return val ? (val + 'px') : ''
},
getHeight(val) {
return val ? (val + 'px') : ''
},
handleRemoteMethod(val, field, parentField, info) {
this.$emit("remote-method", val, field, parentField, info);
},
handleSelectChange(val, field, parentField, info) {
this.$emit("select-change", val, field, parentField, info);
},
setDistrictId(val, label, info, districtLevel) {
this.$emit("address", val, label, info, districtLevel);
},
handleInputBlur(val, field, parentField) {
this.$emit("input-blur", val, field, parentField);
},
beforeAvatarUpload(file, info) {
let name = file.name.slice(0, file.name.lastIndexOf("."));
let validate = validateFileName(name);
if (validate) {
this.$commonMessage.message({
type: "warning",
message: '上传的文件名称不能包含下列字符: \ / : * ? " < > |'
});
return false;
}
if (info.uploadType == "image") {
// 获取不到dwg图标的类型,只能根据name截取后缀判断
let fileTypes = file.name.split(".");
let type = fileTypes[fileTypes.length - 1];
const isJPG =
file.type === "image/jpeg" ||
file.type === "image/png" ||
file.type === "application/pdf" ||
type == "dwg";
if (!isJPG) {
this.$commonMessage.message({
type: "warning",
message: "请上传PNG、JPG、pdf或dwg格式的文件!"
});
return false;
}
if (file.type === "application/pdf" || type == "dwg") {
this.imgUploadList.push({
file: file,
isUpload: false
});
this.$emit("uploadLoad", this.imgUploadList, info);
return true;
} else {
const isLtM = file.size / 1024 / 1024 >= 0.6;
return new Promise(resolve => {
this.imgUploadList.push({
file: file,
isUpload: false
});
this.imgLoading.push(info.field);
this.$emit("uploadLoad", this.imgUploadList, info);
// 小于1M 不压缩
if (!isLtM) {
resolve(file);
}
// 压缩到400KB,这里的400就是要压缩的大小,可自定义
this.imageConversion.compressAccurately(file, 400).then(res => {
resolve(res);
});
});
}
} else {
this.imgUploadList.push({
file: file,
isUpload: false
});
this.imgLoading.push(info.field);
this.$emit("uploadLoad", this.imgUploadList, info);
return true;
}
},
handleExceed(e, limit, info) {
let times = limit ? limit : 3;
let msg = `最多可以上传${times}张${info.label}!`;
if (info.uploadType == "file") {
msg = `最多可以上传${times}个文件!`;
}
this.$commonMessage.message({
type: "warning",
message: msg
});
},
handleAvatarSuccess(res, file, row) {
// let urls = ['file1', 'file2', 'file3', 'file4', 'file5']
// var element = urls[Math.floor(Math.random() * urls.length)]
// 以下是向后端识别图片接口传递file文件
var formData = new FormData();
formData.append("file", res.file);
uploadImg(formData)
.then(response => {
this.$emit("upload", response, file, row);
this.$set(
this.imgUploadList[
this.imgUploadList.findIndex(el => el.file.uid == res.file.uid)
],
"isUpload",
true
);
this.imgLoading.splice(
this.imgLoading.findIndex(el => el == row.field),
1
);
this.$emit("uploadLoad", this.imgUploadList, row);
})
.catch(e => {
this.imgLoading.splice(
this.imgLoading.findIndex(el => el == row.field),
1
);
this.handleRemove(res.file, row);
});
},
handleRemove(file, info, type) {
if (info.disabled) return;
if (
this.imgUploadList.length > 0 &&
this.imgUploadList.findIndex(el => el.file.uid == file.uid) != -1
) {
this.$set(
this.imgUploadList[
this.imgUploadList.findIndex(el => el.file.uid == file.uid)
],
"isUpload",
true
);
this.imgUploadList.splice(
0,
this.imgUploadList.findIndex(el => el.file.uid == file.uid)
);
this.$emit("uploadLoad", this.imgUploadList, info);
}
if (type != "btn") {
if (info.isDynamic) {
this.$refs["upload" + info.field][0].handleRemove(file);
} else {
this.$refs["upload"][0].handleRemove(file);
}
}
this.$emit("upload", file, "delete", info);
},
handleClear() {
this.imgUploadList = [];
},
handleEditPreviewPdf(file, info) {
if (
this.openPdfEditLoading.indexOf(file.code) != -1 &&
this.openPdfEditLoading.indexOf(file.uid) != -1
)
return;
let requestUrl = file.url
? file.url.substring(file.url.indexOf("/upload"), file.url.length)
: "";
if (!requestUrl) {
return;
}
let code = file.code ? file.code : file.uid;
this.openPdfEditLoading.push(code);
axiosGet(requestUrl)
.then(res => {
downLoadFile(res, "", "pdf", "preview");
this.hidePreviewLoading(file);
})
.catch(() => {
this.hidePreviewLoading(file);
});
},
hidePreviewLoading(file) {
let code = file.code ? file.code : file.uid;
this.openPdfEditLoading.splice(
this.openPdfEditLoading.findIndex(el => el == code),
1
);
},
handleDownLoadEdit(file, info) {
if (
this.downLoading.indexOf(file.code) != -1 &&
this.downLoading.indexOf(file.uid) != -1
)
return;
let requestUrl = file.url
? file.url.substring(file.url.indexOf("/upload"), file.url.length)
: "";
if (!requestUrl) {
return;
}
let code = file.code ? file.code : file.uid;
this.downLoading.push(code);
let name = file.name;
axiosGet(requestUrl)
.then(res => {
downLoadFile(res, name);
this.hideImgLoading(file);
})
.catch(() => {
this.hideImgLoading(file);
});
},
hideImgLoading(file) {
let code = file.code ? file.code : file.uid;
this.downLoading.splice(
this.downLoading.findIndex(el => el == code),
1
);
},
handleEditPreview(file, info) {
let code = file.code ? file.code : file.uid;
this.canEditImgLoading.push(code);
this.formImgData = [];
for (let i = 0; i < info.fileList.length; i++) {
let itemImg = info.fileList[i];
if (this.commonFunc.judgeImg(itemImg.name)) {
this.formImgData.push(info.fileList[i]);
}
}
setTimeout(() => {
this.canEditImgLoading.splice(this.canEditImgLoading.indexOf(code), 1);
let a = document.querySelector(`.viewerImg${code}${info.field}`);
a.click();
}, 1000);
},
onDeleteKeyDown(e, field) {
let length = field ? field.length : 0;
if (length != 0) {
let { selectionStart, selectionEnd } = e.target;
// 如果包含不能删除的区域位置,阻止删除
if (!(selectionStart > length || selectionEnd < 0)) {
e.preventDefault();
}
}
},
onKeyDown(e, field) {
let length = field ? field.length : 0;
if (field) {
// 如果包含不能删除的区域,禁止
if (e.target.selectionStart < length || e.target.selectionEnd < 0) {
e.preventDefault();
}
}
},
handleFocusTextarea(e, field) {
if (!(e.target.value && field && field.length)) return;
if (e.target.value.length == field.length) {
setTimeout(() => {
e.target.setSelectionRange(field.length + 1, field.length + 1);
});
}
},
// 输入法键盘字符下不可输入,记录输入的起始位置
onCompositionStart(e, field) {
if (field) {
this.checkText = e.data; //记录选中的文字
this.startVal = e.target.selectionStart;
}
},
// 当在不可编辑的范围内输入是,结合输入文字开始位置this.startVal和输入结束
// e.target.selectionEnd位置,通过字符串截取并接还原原来的字符。
compositionend(e, field) {
let length = field ? field.length : 0;
if (field) {
if (this.startVal !== null) {
this.$nextTick(() => {
if (this.startVal < length) {
let targetVal = e.target.value;
let startTarget = targetVal.substring(0, this.startVal);
let endTarget = targetVal.substring(e.target.selectionEnd);
e.target.value = startTarget + this.checkText + endTarget;
}
});
}
}
}
},
mounted() {}
};
</script>
<style scoped lang="scss"></style>
- 重要部分
- 模板部分(template): 包含了整个表单组件的布局和结构,通过使用Element UI组件库中的el-form、el-form-item等组件来构建表单项。
- 脚本部分(script): 定义了表单组件的逻辑处理部分,包括对表单数据的处理、事件处理函数的定义、计算属性的定义等。
- 样式部分(style): 包含了组件的样式定义,使用了scoped属性来使得样式只作用于当前组件。
- 组件引入部分: 引入了一些自定义组件和第三方组件,如HSelect、HDatePicker、addressCascader等。
- HSelect:详见该篇文章vue elementUI el-select的封装
- HDatePicker:详见该篇文章vue elementUI el-date-picker的封装
- addressCascader:详见该篇文章vue elementUI el-cascader的封装
- HSelectMultiple:详见该篇文章vue elementUI el-select多选的封装
- HRadioGroup:详见该篇文章vue elementUI el-radio-group的封装
- commonTooltip:详见该篇文章Vue tooltip 组件封装
- viewer:详见该篇文章v-viewer----底部title显示图片的名称
- 方法引入部分:
- downLoadFile: 详见该篇文章vue 全局封装文件下载及导入
- validateFileName: 详见该篇文章检验文件命名
- this.$commonMessage.message():详见该篇文章vue elementUI 消息提示封装
页面引用
- 在views下创建名为index.vue的文件
<template>
<div
class="common-pop-box common-scroll"
>
<h-common-pop :title="title" ref="popBox" :footerList="footerBtnList">
<h-common-form-item
:formList="formList"
:hideRequiredAsterisk="detailInfo.type=='detail'"
@select-change="handleSelectChange"
@address="handleSelectAddress"
@upload="handleImgInfo"
@uploadLoad="handleImgLoad"
ref="myFromItem"
>
</h-common-form-item>
</h-common-pop>
</div>
</template>
<script>
import HCommonPop from "@/components/commonPop/index";
import HCommonFormItem from "@/components/commonFormItem/index";
import commonFormMixin from "@/mixins/commonForm";
export default {
mixins: [commonFormMixin],
data () {
return {
needBottom: false,
formList: [
{
title: '日期/创建人',
key: 'first',
formValidate: {},
purchasingInformation: {
form: {
createTime: '',
createUserName: ''
},
formLabel: [
{
type: 'text',
field: 'createTime',
label: '创建日期',
display: true,
disabled: true,
wordBold: true,
itemWidth: '48%',
marginRight: '4%',
icon: 'date',
widthIsNotNum: true
},
{
type: 'text',
field: 'createUserName',
label: '创建人',
display: true,
disabled: true,
wordBold: true,
align: 'right',
widthFit: true,
itemWidth: '48%',
icon: 'operator',
widthIsNotNum: true
}
]
}
},
{
title: '基本信息',
key: 'second',
formValidate: {
type: [
{ required: true, message: '请选择类型', trigger: 'change' }
],
name: [
{ required: true, message: '请输入名称', trigger: 'blur' },
{
min: 1,
max: 50,
message: '名称长度在 1 到 50 个字符',
trigger: 'blur'
}
],
contactName: [
{ required: true, message: '请输入联系人', trigger: 'blur' },
{
min: 1,
max: 5,
message: '联系人长度在 1 到 5 个字符',
trigger: 'blur'
}
],
contactPhone: [
{ required: true, message: '请输入电话1', trigger: 'blur' },
{ trigger: 'blur', validator: this.validTelephone }
],
contactPhoneTwo: [
{ required: false, message: '请输入电话2', trigger: 'blur' },
{ trigger: 'blur', validator: this.validTelephone2 }
],
companyDistrictId: [
{ required: true, message: '请选择地址', trigger: 'change' }
],
companyAddress: [
{
required: true,
message: '请输入详细地址',
trigger: 'blur'
},
{
min: 1,
max: 50,
message: '详细地址长度在 1 到 50 个字符',
trigger: 'blur'
}
]
},
purchasingInformation: {
form: {
type: '',
name: '',
contactName: '',
contactPhone: '',
contactPhoneTwo: '',
companyDistrictId: '',
companyAddress: '',
file: ''
},
formLabel: [
{
type: 'select',
field: 'type',
label: '类型',
clearable: true,
marginBottom: true,
placeholder: '选择类型',
display: true,
selectUrl: '/core/select/type',
props: {
label: 'name',
value: 'value'
},
disabled: false,
itemWidth: 330,
marginRight: 59,
icon: 'clientType',
options: []
},
{
type: 'text',
field: 'name',
label: '名称',
clearable: true,
selectPlaceholderAlign: 'right',
disabledAlign: 'right',
display: true,
disabled: false,
itemWidth: 1038,
icon: 'supplier'
},
{
type: 'text',
field: 'contactName',
label: '联系人',
marginBottom: true,
display: true,
disabled: false,
itemWidth: 411,
marginRight: 194,
icon: 'contacts'
},
{
type: 'text',
field: 'contactPhone',
label: '电话1',
display: true,
disabled: false,
itemWidth: 339.5 * 2,
marginRight: 262 * 2,
icon: 'phone'
},
{
type: 'text',
field: 'contactPhoneTwo',
label: '电话2',
display: true,
disabled: false,
itemWidth: 340,
marginRight: 0,
icon: 'phone'
},
{
type: 'district',
field: 'companyDistrictId',
label: '公司地址',
display: true,
disabled: false,
marginBottom: true,
itemWidth: 672.5,
marginRight: 30,
icon: 'position'
},
{
type: 'text',
field: 'companyAddress',
label: '详细地址',
noLabel: true,
placeholder: '填写详细地址',
display: true,
disabled: false,
inputPlaceHolderAlign: 'right',
itemWidth: 847
},
{
type: 'upload',
field: 'file',
uploadType: 'image',
label: '文件',
isDynamic: true,
multiple: true,
display: false,
disabled: false,
itemWidth: '100%',
widthIsNotNum: true,
icon: 'agreement',
chooseImgListOrigin: [],
uploadImgList: [],
fileList: [],
chooseImgList: []
}
]
}
}
],
agreementInfo:{}
}
},
components: {
HCommonPop,
HCommonFormItem
},
computed: {
footerBtnList () {
return [
{
caption: '返回',
type: 'warning',
callback: this.back,
btnClass: 'warningButton',
},
{
caption: '保存',
type: 'primary',
callback: this.submit,
btnClass: 'primaryButton',
}
]
}
},
mounted () {
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.common-pop-box {
width: 100%;
height: 100%;
padding-bottom: 34px;
box-sizing: border-box;
&.common-pop-box-footer {
::v-deep .footer-button-group {
margin-top: 200px;
}
}
}
</style>
- 页面中使用了两个子组件,引入了一个mixins:
- HCommonPop: 定义了一个包含标题和右侧插槽的div容器。
- HCommonFormItem: 详见上文
- commonFormMixin: 处理表单数据、上传文件、校验表单、获取下拉选项,提交表单;具体处理详见该篇文章vue 表单封装后数据的获取提交
HCommonPop
标题会根据传入的title属性进行显示,如果没有传入则显示空字符串。右侧插槽可以在使用该组件时插入其他内容。接着,使用了slot标签来插入组件的内容,即弹出窗口的主体部分。最后,通过引入HCommonFooter组件,将按钮列表传递给该组件,并在footerList有内容时显示通用底部组件
<template>
<div>
<div
class="pop-title"
>
<slot name="pop-title">
{{ title }}
<slot name="pop-right"></slot>
</slot>
</div>
<slot></slot>
<h-common-footer :buttonList="footerList" v-if="footerList.length>0"> </h-common-footer>
</div>
</template>
<script>
import HCommonFooter from '@/components/commonFooter/index'
export default {
name: 'HCreatePop',
props: {
title: {
type: String,
default: () => {
return ''
}
},
footerList: {
type: Array,
default: () => {
return []
}
}
},
components: { HCommonFooter },
data () {
return {
visible: true
}
},
created () {},
mounted () {},
methods: {
},
watch: {
dialogVisible (val) {
this.visible = this.dialogVisible
},
},
computed: {
}
}
</script>
<style lang="scss" scoped>
.pop-title {
width: calc(100% - 3.54167vw);
text-align: center;
height: 48px;
font-weight: 600;
font-size: 40px;
z-index: 0;
color: #000;
display: flex;
align-items: center;
justify-content: center;
margin-top: 13px;
position: relative;
margin-bottom: 88px;
}
</style>
HCommonFooter
动态生成button的组件
<template>
<div
class="footer-button-group"
:class="[
isAllWidth ? 'footer-button-group-all' : '',
isSmallBtn ? 'footer-button-group-small-bth': ''
]"
>
<template
v-for="(
{ caption, display, permissionKey, icon, disabled, callback, action, type, btnClass, loading },
index
) in buttonList"
>
<h-button v-if="getButtonDisplay(display)"
:key="action ? index + action : index"
:action="action"
:btnClass="btnClass"
:caption="caption"
:icon="icon"
:disabled="disabled"
:type="type || 'primary'"
:loading="loading"
@click="callback"
v-permission="permissionKey"
></h-button>
</template>
</div>
</template>
<script>
import HButton from '@/harley-ui/button'
export default {
data() {
return {}
},
components: {
HButton
},
props: {
isAllWidth: {
default: false,
type: Boolean
},
isSmallBtn: {
default: false,
type: Boolean
},
buttonList: {
type: Array,
caption: [String, Number],
icon: String,
disabled: {
type: Boolean,
default: false
},
callback: {
type: Function,
required: true
},
action: {
type: String,
validator(value) {
return ['add', 'edit', 'delete', 'appointRow'].indexOf(value) !== -1
}
},
loading: {
type: Boolean,
default: false
}
}
},
methods: {
getButtonDisplay(display) {
let result = true
if (typeof display === 'boolean') {
result = display
} else if (typeof display === 'function') {
result = display()
}
return result
},
}
}
</script>
<style lang="scss" scoped>
.footer-button-group {
margin-top: 60px;
box-sizing: border-box;
width: calc(100% - 3.54167vw);
display: flex;
align-items: center;
justify-content: center;
&.footer-button-group-all {
width: 100%;
}
}
</style>
- 模板部分(template):
- 通过v-for指令循环遍历buttonList数组,动态生成按钮组。根据buttonList中的数据来渲染h-button组件,实现按钮的动态生成。
- 根据getButtonDisplay方法返回的布尔值来决定按钮是否显示。
- 脚本部分(script):
- 导入HButton组件,并在components属性中注册。
- HButton:详见该篇文章vue elementUI el-button的封装
- 定义了props属性,用于接收父组件传递的数据。其中isAllWidth和isSmallBtn是布尔类型的prop,buttonList是数组类型的prop,包含了按钮的各种属性和回调函数。
- 导入HButton组件,并在components属性中注册。
- 方法部分:
- getButtonDisplay方法用于根据按钮的display属性确定按钮是否显示。如果display是布尔值,则直接返回;如果是函数,则执行函数并返回结果。
- 样式部分(style):
- 使用了SCSS语法定义了.footer-button-group类的样式。设置了按钮组的样式,包括顶部间距、宽度、排列方式等。根据isAllWidth和isSmallBtn属性的值,可以动态改变按钮组的样式。