背景:
在前端系統開發中,系統頁面涉及到的表單組件比較多,所以進行了簡單的封裝。封裝的包括一些Form表單組件,如下:input輸入框、select下拉框、等
實現效果:
理論知識:
表單組件官方鏈接:點擊跳轉
封裝組件:
封裝組件的思路:
?不封裝element組件,每一個input組件綁定一個form對象,例如官網。
簡單封裝element組件,利用for循環生成form表單的每一項el-form-item。
進一步封裝,利用判斷表單組件的類型,是input、select或者其它表單組件。
終極封裝,把el-form表單封裝成一個獨立的表單,自定義命名為AreaFormItem.vue;然后單頁面vue調用AreaFormItem.vue傳參以及表單數據回顯等操作。
封裝組件的過程:?
1.不封裝表單組件,代碼如下:?
實現代碼:【表單組件未封裝】
<el-form ref="ruleForm" :model="state.form" label-width="auto" :rules="rules" class="form-box"><el-row class="form-row"><el-col :span="5" :gutter="20" style="padding-left: 0px"><el-form-item label="航次" prop="voyageNo"><el-input v-model="state.form.voyageNo" placeholder="請輸入航次" /></el-form-item></el-col><el-col :span="4" :gutter="20" style="padding-left: 10px"><el-form-item label="船名" prop="name"><el-input v-model="state.form.name" placeholder="請輸入船名" /></el-form-item></el-col><el-col :span="4" :gutter="20" style="padding-left: 10px"><el-form-item label="裝港" prop="loadName"><el-input v-model="state.form.loadName" placeholder="請輸入裝港" /></el-form-item></el-col><el-col :span="5" :gutter="20" style="padding-left: 10px"><el-form-item label="卸港" prop="dischargName"><el-input v-model="state.form.dischargName" placeholder="請輸入卸港" /></el-form-item></el-col><el-col :span="5" :gutter="20" style="padding-left: 10px"><el-form-item label="卸港" prop="cargoType"><el-input v-model="state.form.cargoType" placeholder="請輸入卸港" /></el-form-item></el-col></el-row></el-form>
備注:因為是橫著的表單,這里加入了el-row和el-col?。
2.表單組件封裝,for循環渲染表單組件input:
//html
<el-form ref="ruleForm" :model="state.form" label-width="auto" :rules="rules" class="form-box"><el-row class="form-row"><el-col :span="item.span || 5" :gutter="20" style="padding-right: 10px" v-for="(item, index) in formItems":key="index"><el-form-item :label="item.label" :prop="item.key"><el-input v-model="state.form[item.key]" :placeholder="item.placeholder || '請輸入'" /></el-form-item></el-col></el-row></el-form>//涉及到的變量
const state = reactive({form: {name: '',voyageNo: '',loadName: '',dischargName: '',cargoType: ''},
})
const formItems = [{type: 'input',label: '船名',key: 'name',placeholder: '請輸入或者選擇船名'},{type: 'input',label: '航次',key: 'voyageNo',placeholder: '請輸入或者選擇航次'},{type: 'select',label: '裝港',key: 'loadName',placeholder: '請輸入或者選擇裝港'},{type: 'select',label: '卸港',key: 'dischargName',placeholder: '請輸入或者選擇始港'},{type: 'input',label: '貨種',span : 4,key: 'cargoType',placeholder: '請輸入或者選擇貨種',}
]
3.封裝一個input或者select下拉列表,封裝如下:
<el-form ref="ruleForm" :model="state.form" label-width="auto" :rules="rules" class="form-box"><el-row class="form-row"><el-col :span="item.span || 5" :gutter="20" style="padding-right: 10px"v-for="(item, index) in formItems" :key="index"><el-form-item :label="item.label" :prop="item.key"><el-input v-model="state.form[item.key]" :placeholder="item.placeholder || '請輸入'"v-if="item.type === 'input'" /><template v-else-if="item.type === 'select'"><el-select v-model="state.form[item.key]" :clearable="true":loading="routeState.loading_startPoint" :multiple="false":remote-method="(query) => remoteMethod_Point(query, 'cargoType')"filterable placeholder="請輸入或者選擇貨種" remote reserve-keyword@change="(value) => blur_Point(value, 'cargoType', '')"@clear="clear_select('cargoType')"><el-option v-for="item in routeState.options_startPoint" :key="item.value":label="item.label" :value="item.label" /></el-select></template></el-form-item></el-col></el-row></el-form>
//涉及到的數據
const routeState = reactive({loading_startPoint:false,options_startPoint:[]
})
4.終極封裝,把上面的form表單封裝成一個vue組件,自定義命名為?AreaFormItem.vue,
AreaFormItem.vue組件,封裝如下:
<template><el-form :model="data.formInline" class="area-from" :inline="true" :label-position="'left'" ref="ruleAreaFormRef":rules="data.rules"><template v-for="(item, index) in props.formItems" :key="index"><template v-if="item.type &&!['location','medium','password','sexRadio','coordinate','radio','textarea','multipleSelect','uploadFile','uploadSingleImg',].includes(item.type)"><el-form-item :label="item.label" :label-width="item.labelWidth" :prop="item.key" :class="item.class" :style="{'max-width': item.width || '50%',width: item.specialWidth || item.width,}" v-if="item.linkageShow === undefined || item.linkageShow"><template v-if="item.type == 'select'"><el-select v-model="data.formInline[item.key]" placeholder="請選擇":style="{ width: item.specialWidth || item.width || '10vw' }" :disabled="item.isEditAbled && data.isAbled":filterable="item.filterable" :filter-method="(filterVal) => {handleSelectEdit(filterVal, item);}" @blur="() => {item.filterable &&data.editSelectValue &&(data.formInline[item.key] = data.editSelectValue);}" @change="(val) => {handleSelectEdit(val, item);searchChange(item.linkageKey,item.optionsData,data.formInline[item.key],item.linkageDraw,item.autoPosition,item.dynamicLinkageKey,props.formItems,item.linkageKeyList,item);}"><template v-if="item.optionsData.length"><el-option v-for="(optionItem, index) in item.optionsData" :key="index" :label="optionItem.label":value="optionItem.value" /></template><template v-else><el-option v-for="(optionItem, index) in data.selectData" :key="index" :label="optionItem.label":value="optionItem.value" /></template></el-select></template><template v-if="item.type == 'selectTree'" style=".el-select {width: '10vw';}"><el-tree-select v-model="data.formInline[item.key]" :placeholder="item.placeholder || '請選擇所屬區域'":style="{ width: item.specialWidth || item.width || '10vw' }" :data="data.treeData":props="data.defaultProps" check-strictly clearable :multiple="item.multiple || false":disabled="item.disabled || false" @change="(value) => handlechange(value, item.limit, item.isJump || true)" :filterable="item.filterable || false"></el-tree-select></template><template v-if="item.type == 'treeMuiltSelect'" style=".el-select {width: '10vw';}"><el-tree-select v-model="data.formInline[item.key]" :placeholder="item.placeholder || '請選擇渡口'":style="{ width: item.specialWidth || item.width || '10vw' }" :data="data.treeDataList":props="item.optionParam || data.defaultProps" :multiple="item.multiple || false" :collapse-tags="true":collapse-tags-tooltip="true" :max-collapse-tags="1" clearable :disabled="item.disabled || false"></el-tree-select></template><template v-if="item.type == 'treeLocationSelect'" style=".el-select {width: '10vw';}"><el-tree-select v-model="data.formInline[item.key]" :placeholder="item.placeholder || '請選擇區域下的渡口'":style="{ width: item.specialWidth || item.width || '10vw' }" :data="data.treeLocationSelect":props="item.optionParam || data.defaultProps" :multiple="item.multiple || false" :collapse-tags="true":collapse-tags-tooltip="true" :max-collapse-tags="1" clearable :disabled="item.disabled || false" :check-strictly="false"default-expand-all @change="(value) => handleSelectchange(value, item)":filterable="item.filterable || false" :filter-method="(filterVal) => {filtertreeLocationSelect(filterVal, item, 'treeLocationSelect');}" @blur="blur(item, 'treeLocationSelect')"></el-tree-select></template><template v-if="item.type == 'remoteSelect'"><el-select v-model="data.formInline[item.key]" filterable remote :placeholder="item.placeholder"remote-show-suffix :remote-method="(query) => {remoteMethod(query, item);}" :loading="data.remoteSelectLoading" @change="(val) => {item.getSelectRemoteValue &&item.getSelectRemoteValue(val, data.remoteSelectData);}" :disabled="item.isEditAbled && data.isAbled"><el-option v-for="optionItem in data.remoteSelectData" :key="optionItem.value" :label="optionItem.label":value="optionItem.value" /></el-select></template><template v-if="item.type == 'input'"><el-input v-model.trim="data.formInline[item.key]" placeholder="請輸入" clearable @clear="searchChange":disabled="(item.isEditAbled && data.isAbled) || item.disabled":style="{ width: item.inputWidth || '10vw' }" :readonly="item.isReadonly || false"><template #suffix v-if="item.suffix">{{ item.suffix }}</template></el-input></template><template v-if="item.type == 'checkBox'"><div class="warn-type"><el-checkbox v-model="checkAll" :indeterminate="isIndeterminate" @change="(state) => {handleCheckAllChange(state, item.key);}" v-if="item.optionCheckAll">全部</el-checkbox><el-checkbox-group v-model="data.formInline[item.key]" @change="handleCheckedChange"><el-checkbox v-for="checkItem in item.optionsData" :key="checkItem.value" :label="checkItem.value":value="checkItem.value">{{ checkItem.label }}</el-checkbox></el-checkbox-group></div></template><template v-if="item.type == 'inputNumber'"><el-input v-model.trim.number="data.formInline[item.key]" placeholder="請輸入數字" clearable@clear="searchChange" :disabled="item.isEditAbled && data.isAbled" style="width: 10vw":readonly="item.isReadonly || false" oninput="value = value.replace(/[^\d.]/g,'')"><template #suffix v-if="item.suffix">{{ item.suffix }}</template></el-input></template><template v-if="item.type == 'inputPhoneNumber'"><el-input v-model.trim.number="data.formInline[item.key]" placeholder="請輸入數字" clearable@clear="searchChange" :disabled="item.isEditAbled && data.isAbled":style="{ width: item.inputWidth || '10vw' }" :readonly="item.isReadonly || false"oninput="value = value.replace(/[^\d.]/g,'')"><template #suffix v-if="item.suffix">{{ item.suffix }}</template></el-input></template><template v-if="item.type == 'cascader'"><el-cascader v-model="data.formInline.cityValue" :options="data.citiesData" :props="data.cityDefalut"@change="citySelectChange" style="width: 10vw" /></template><template v-if="item.type == 'datePicker'"><el-date-picker v-model="data.formInline[item.key]" type="datetime" placeholder="請選擇":default-time="item.defaultTime || defaultTime" value-format="YYYY-MM-DD HH:mm:ss":style="{ width: item.specialWidth || '10vw' }" :readonly="item.isReadonly || false":disabled-date="item.disabledDate" /></template><template v-if="item.type == 'timePicker'"><el-time-picker v-model="data.formInline[item.key]" placeholder="請選擇時間":default-time="item.defaultTime || defaultTime" value-format="HH:mm:ss":style="{ width: item.specialWidth || '10vw' }" :readonly="item.isReadonly || false":disabled-date="item.disabledDate" /></template><template v-if="item.type == 'dateNoTimePicker'"><el-date-picker v-model="data.formInline[item.key]" type="date" placeholder="請選擇":default-time="item.defaultTime || defaultTime" value-format="YYYY-MM-DD":style="{ width: item.specialWidth || '10vw' }" :readonly="item.isReadonly || false":disabled-date="item.disabledDate" /></template></el-form-item></template><template v-if="item.type == 'multipleSelect'"><div class="select-area"><el-form-item :label="item.label" :label-width="item.labelWidth" :prop="item.key"><el-select v-model="data.formInline[item.key]" multiple clearable collapse-tags:placeholder="item.placeholder" popper-class="custom-header" :max-collapse-tags="1"style="width: 100%; height: 50px" @change="(val) => {item.selectOptionChange(val, item);}"><template #header v-if="item.optionsData.length"><el-checkbox v-model="item.checkAll" :indeterminate="item.indeterminate" @change="(val) => {item.handleCheckAll(val, item, data.formInline);}">全部區域</el-checkbox></template><el-option v-for="(optionItem, index) in item.optionsData" :key="index" :label="optionItem.label":value="optionItem.value" /></el-select></el-form-item></div></template><template v-if="item.type == 'radio'"><div class="warning-level" v-if="item.editShow === undefined ? true : !data.isAbled && !item.editShow" :style="{display:(item.onlySingleSelect || item.onlySingleSelect) &&'inline-block',width: item.width || '100%',}"><el-form-item :label="item.label" :label-width="item.labelWidth" :prop="item.key"v-if="!item.onlyDrawType && !item.hiddenFormItem"><el-radio-group v-model="data.formInline[item.key]" @change="(val) =>item.linkageAction? finallyLinkageSolve(item): warningLevelChange(val)"><template v-if="item.onlySingleSelect"><el-radio v-for="(aitem, aindex) in item.optionsData" :key="aindex" :label="aitem.value">{{ aitem.label}}</el-radio></template><template v-else><el-radio :label="1">一級</el-radio><el-radio :label="2">二級</el-radio><el-radio :label="3">三級</el-radio></template></el-radio-group></el-form-item><el-form-item :label="'繪制類型:'" :label-width="item.labelWidth" :prop="'drawType'"v-if="!item.onlySingleSelect && data.showDraw"><el-select v-model="data.formInline.drawType" placeholder="請選擇" @change="drawTypeChange"style="width: 10vw"><el-option v-for="(optionItem, index) in data.drawTypes" :key="index" :label="optionItem.label":value="optionItem.value" /></el-select></el-form-item></div></template><template v-if="item.isBufferArea && !item.hiddenFormItem"><div class="warning-distance"><template v-for="(bufferItem, bufferIndex) in data.bufferData" :key="bufferIndex"><template v-if="bufferItem.level <= data.formInline.level"><el-form-item :label="bufferItem.label" :label-width="bufferItem.labelWidth" :prop="bufferItem.key"><el-input v-model.trim="data.formInline[bufferItem.key]" style="width: 4.2vw" placeholder="請輸入"clearable></el-input><span class="unit">m</span></el-form-item></template></template></div></template><template v-if="item.type && item.type == 'textarea'"><div class="area-remark" :style="{ 'margin-top': item.marginTop || '0px' }"v-show="!item.conditionShow || item.conditionShow(props.formItemsVal)"><el-form-item :label="item.label" :label-width="item.labelWidth" :prop="item.key" :class="item.class"><el-input v-model="data.formInline[item.key]" :placeholder="item.placeholder || '請輸入備注信息'" type="textarea":disabled="item.isEditAbled && data.isAbled" /></el-form-item></div></template><template v-if="item.type && item.editShow === undefined? item.type == 'coordinate': item.type == 'coordinate' && !data.isAbled && !item.editShow"><div class="cordinate-container"><el-form-item :label="item.label" :label-width="item.labelWidth" :prop="item.key"><div class="wrapper"><div class="row-input-box" v-for="(columnItem, index) in tableCloumn" :key="index"><div class="row-name">{{ columnItem.label }}</div><div class="input-item" v-for="(inpItem, ipIndex) in columnItem.inpModelParam" :key="ipIndex"><el-tooltip placement="top" :manual="true" :content="inpItem.msg"><el-input v-model="data.coordinate[inpItem.value]"></el-input></el-tooltip><div class="inp-unit">{{ inpItem.label }}</div></div></div></div><div class="box"><img :src="getAssetsFile('buttons/select_point.png')" @click="drawArea" /></div></el-form-item></div></template><template v-if="item.type == 'sexRadio'"><el-form-item :label="item.label" :label-width="item.labelWidth" :prop="item.key"><el-radio-group v-model="data.formInline[item.key]"><el-radio :label="1">男</el-radio><el-radio :label="2">女</el-radio></el-radio-group></el-form-item></template><template v-if="item.editShow === undefined? item.type == 'password': item.type == 'password' && !data.isAbled && !item.editShow"><el-form-item :label="item.label" :label-width="item.labelWidth" :prop="item.key" :class="item.class"><el-input v-model="data.formInline[item.key]" placeholder="請輸入" type="password" autocomplete="new-password"show-password style="width: 10vw" /></el-form-item></template><template v-if="item.type == 'uploadFile'"><el-form-item :label="item.label" :label-width="item.labelWidth" :prop="item.key"><el-input v-model="data.formInline[item.key]" :placeholder="item.placeholder || data.headerResult.placeholder || '請輸入'" clearable style="width: 245px" @clear="(value) =>fileClear('fileClear', data.headerResult.fileName, item)"><template #append><div class="opration-box" v-if="item.option"><div v-for="oprationItem in item.option" :key="oprationItem.text"><el-tooltip class="box-item" effect="dark" :content="oprationItem.text" placement="top"><template v-if="oprationItem.info == 'upload'"><el-upload ref="uploadRef" class="upload-demo" :show-file-list="false" :auto-upload="true"accept=".docx,.pdf,.doc" :action="oprationItem.params.importUrl":data="oprationItem.uploadParames" :headers="{ Authorization: store.userInfo.token }":on-success="(info) => handleResult('success', info, oprationItem)" :on-error="(info) => handleResult('error', info)"><img :src="upIcon" style="padding: 0 7px" /></el-upload></template><template v-else><img :src="upIcon" style="padding: 0 7px" @click="handleOpenDialog(oprationItem.params, oprationItem)" /></template></el-tooltip></div></div></template></el-input></el-form-item></template><template v-if="item.type == 'uploadMuliteImg'"><el-form-item :label="item.label" :label-width="item.labelWidth" :prop="item.key" :style="{width: item.specialWidth || item.width || '10vw',maxWidth: item.specialWidth || item.width || '10vw',}"><el-upload :auto-upload="false" list-type="picture-card" :on-preview="handlePictureCardPreview":on-change="uploadSingleImg" :limit="5" :file-list="upImgState.fileList"><el-icon v-if="upImgState.fileList.length == 0"><Plus /></el-icon></el-upload><el-dialog v-model="upImgState.dialogVisible"><img :src="upImgState.dialogImageUrl" alt="Preview Image" style="width: 100%" /></el-dialog></el-form-item></template><template v-if="item.type == 'uploadSingleImg'"><el-form-item :label="item.label" :label-width="item.labelWidth" :prop="item.key" :style="{width: item.specialWidth || item.width || '10vw',maxWidth: item.specialWidth || item.width || '10vw',}"><el-upload list-type="picture-card" :limit="1" :file-list="data.formInline[item.key]" :style="{width: item.width || '10vw',height: item.width || '10vw',}" :action="item.option.importUrl" :data="item.option.uploadParames" :show-file-list="true":headers="{ Authorization: store.userInfo.token }":on-success="(info) => uploadSingleImg('success', info, item)":on-error="(info) => uploadSingleImg('error', info)" :before-upload="beforeUploadSingleImg" :before-remove="(info) => beforeRemoveSingleImg('remove', info, item)" :on-preview="handlePictureCardPreview" accept="image/jpeg,image/png" :disabled="data.formInline[item.key] && data.formInline[item.key].length? true: false"><el-icon class="avatar-uploader-icon"><Plus /></el-icon><template #file="{ file }"><div><img class="el-upload-list__item-thumbnail" :src="file.url" alt="" /><span class="el-upload-list__item-actions"><span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)"><el-icon><zoom-in /></el-icon></span><span class="el-upload-list__item-delete" @click="beforeRemoveSingleImg('remove', file, item)"><el-icon><Delete /></el-icon></span></span></div></template></el-upload><el-dialog v-model="upImgState.dialogVisible"><img :src="upImgState.dialogImageUrl" alt="Preview Image" style="width: 100%" /></el-dialog></el-form-item></template></template></el-form>
</template><script setup>
import { max, merge } from "lodash";
import { useAxios } from "@/utils/useAxios";
import axios from "axios";
import { transformObject, getAssetsFile, debounce } from "@/utils";
import { arrayToTreeRec } from "@/utils/utils.js";
import { mapDrawGraph } from "@/utils/map/mapDraw";
import { reactive, ref, watch } from "vue";
import useShoreBasedStore from "@/store/index";
import { BASEUrl } from "@/utils/request";
import { locationPoint } from "@/assets/js/commont";
import upIcon from "@/assets/images/table/upload.png";
const store = useShoreBasedStore();
const defaultTime = new Date();
const tableCloumn = [{label: "經度",prop: "lat",type: "lat",inpModelParam: [{ value: "jd", label: "度", msg: "大于等于0小于等于180的整數" },{ value: "jf", label: "分", msg: "大于等于0小于60的整數" },{ value: "jm", label: "秒", msg: "大于等于0小于60" },],},{label: "緯度",prop: "lon",type: "lon",inpModelParam: [{ value: "wd", label: "度", msg: "大于等于0小于等于90的整數" },{ value: "wf", label: "分", msg: "大于等于0小于60的整數" },{ value: "wm", label: "秒", msg: "大于等于0小于60" },],},
];let props = defineProps({dialogType: {type: String,default: "",},formItems: {type: Array,default: [],},formRules: Object,formItemsVal: Object,MapData: Object,/* tableData: {type: Array,default: [],}, */inputTableData: {type: Array,default: [],},
});
const ruleAreaFormRef = ref();const validateInteger = (rule, value, callback) => {if (Number(value) > 0) callback();else callback(new Error("請輸入大于零的數字"));
};const data = reactive({formInline: {drawType: "3", //繪制類型,默認是面level: "2",tempObj: {}, //臨時的選中lable},coordinate: {jd: "",jf: "",jm: "",wd: "",wf: "",wm: "",},selectData: [],remoteSelectData: [],count: 0, //下拉列表請求接口的 次數bufferData: [{ label: "一級緩沖距離:", level: 1, key: "buffDistance1" },{ label: "二級緩沖距離:", level: 2, key: "buffDistance2" },{ label: "三級緩沖距離:", level: 3, key: "buffDistance3" },],drawTypes: [{value: "1",label: "點",},{value: "2",label: "線",},{value: "3",label: "面",},],rules: {level: [{ required: true, trigger: "change" }],buffDistance1: [{ required: true, message: "一級緩沖距離不能為空", trigger: "blur" },{ validator: validateInteger, rigger: "blur" },],buffDistance2: [{ required: true, message: "二級緩沖距離不能為空", trigger: "blur" },{ validator: validateInteger, rigger: "blur" },],buffDistance3: [{ required: true, message: "三級緩沖距離不能為空", trigger: "blur" },{ validator: validateInteger, rigger: "blur" },],},cityDefalut: {label: "name",value: "code",children: "cities",},citiesData: [],isAbled: false,showDraw: true,clearFlag: null, //聯動值切換時清空初始化標志editSelectValue: "",remoteSelectLoading: false, //可以輸入搜索的下拉框headerApi: {},headerResult: {fileName: "",address: "",placeholder: "點擊右側按鈕進行上傳文件",},resTreeData: [],treeData: [],treeDataList: [],treeLocationSelect: [],storeTrseeData: [],defaultProps: {label: "name",value: "pid",},
});
const emit = defineEmits(["searchChange", "drawTypeChange", "controlDraw"]);
const searchChange = (linkageKey,optionsData,target,linkageDraw,autoPosition,dynamicLinkageKey,list,linkageKeyList,targetItem
) => {if (target?.length == 9) {data.formInline.tempObj = optionsData?.filter((item) => {return item.value === target;});}if (linkageDraw) {data.showDraw = linkageDraw(target);emit("controlDraw", data.showDraw);}if (linkageKey) {if (optionsData.length) {//聯動下拉數據靜態const result = optionsData.find((item) => {return item.value === target;});data.clearFlag = target;if (result?.url) {const buildData = {url: result.url,optionParam: result.optionParam,};getSelectData(buildData);} else {data.selectData = [{label: "假數據",value: "test",},];}} else {//聯動下拉數據動態獲取}}if (autoPosition) {let positionTaget = null;if (data.formInline.itemsString && data.formInline.itemsString === target) {positionTaget = JSON.parse(target).guid;} else {positionTaget = target;}mapData.MapData.location(mapData._layer.layers["windFarmArea"],positionTaget,12,mapData._layer);}if (dynamicLinkageKey) {const linkagedTaget = list.find((item) => item.key === dynamicLinkageKey);data.formInline[dynamicLinkageKey] = "";getSelectData(linkagedTaget, {[linkagedTaget.linkageMoreParamKey]: target,});}if (linkageKeyList) {finallyLinkageSolve(targetItem);}emit("searchChange", data.formInline);
};//船舶白名單預警類型的控制邏輯
const checkAll = ref(true);
const isIndeterminate = ref(false);
const handleCheckAllChange = (val, formItemKey) => {data.formInline[formItemKey] = val ? data.checkedAll : [];isIndeterminate.value = false;
};
const handleCheckedChange = (value) => {const checkedCount = value.length;checkAll.value = checkedCount === data.checkedAll.length;isIndeterminate.value =checkedCount > 0 && checkedCount < data.checkedAll.length;
};
// 最終聯動邏輯解決方案
const finallyLinkageSolve = async (targetItem) => {if (!targetItem.linkageKeyList?.length) return;const targetLinkItems = props.formItems.filter((aitem) =>targetItem.linkageKeyList.includes(aitem.key));if (targetItem.linkageAction) {if (targetItem.linkOpenSetTimeout) {setTimeout(() => {targetItem.linkageAction(data.formInline[targetItem.key],targetLinkItems,targetItem.optionsData,data.formInline);}, 500);} else {let value =data.formInline[targetItem.key] || data.formInline[targetItem.key] == 0? data.formInline[targetItem.key]: props.formItemsVal[targetItem.key];targetItem.linkageAction(value, targetLinkItems, targetItem.optionsData);}}
};
const drawTypeChange = () => {emit("drawTypeChange", data.formInline.drawType);
};
const validateForm = async () => {let result = true;await ruleAreaFormRef.value.validate((valid, fields) => {const coordinateTarget = props.formItems.find((item) => item.type === "coordinate");if (coordinateTarget && fields) {const fieldsKey = Object.keys(fields);if (fieldsKey.length === 1 && fieldsKey[0] === coordinateTarget.key) {if (coordinateTarget.rule?.[0].required) {for (let coorItem in data.coordinate) {if (!data.coordinate[coorItem]) {return (result = false);}}return (result = true);}}}return (result = valid);});let formObj = Object.assign({}, data.formInline);return {isValidate: result,formData: transformObject(formObj, ["cityValue", "drawType", "BufferArea"]),coordinate: data.coordinate,};
};
const resetForm = () => {ruleAreaFormRef.value.resetFields();
};
const getFormItems = () => {const formItemsKeys = props.formItems.map((aitem) => aitem.key).filter(Boolean);return formItemsKeys;
};
const getFromData = () => data.formInline;//切換預警類型的選擇
const warningLevelChange = (val) => {if (val == 1) {data.bufferData.forEach((el) => {if (el.level > val && data.formInline[el.key] != "") {data.formInline[el.key] = "";}});}
};const getSelectData = async (selectOption, moreparams) => {const { resData } = await useAxios({url: selectOption.url,method: "post",param: { pageNo: 1, pageSize: 0, ...moreparams },});if (resData.data && resData.data.length > 0) {let seletData = resData.data.map((item) => {return {label: selectOption.optionParam && item[selectOption.optionParam.label],value:selectOption.optionParam &&selectOption.optionParam.value !== "itemsString"? item[selectOption.optionParam.value]: JSON.stringify(item),};});selectOption.optionsData = seletData;//下拉框需要設置默認值的if (selectOption.default) {data.formInline[selectOption.key] = selectOption.optionsData[0].value;if (selectOption.cablckData) {selectOption.cablckData(selectOption.optionsData);}} else {data.selectData = selectOption.optionsData;if (selectOption.all)selectOption.optionsData.unshift({ label: "全部", value: "" });}} else {selectOption.optionsData = [{ label: "暫無數據", value: "" }];}
};
const getSelectTreeData = async (selectOption, moreparams) => {const { resData } = await useAxios({url: selectOption.url,method: selectOption.methed || "post",param: selectOption?.isNoGetParams? "": { pageNo: 1, pageSize: 10, ...moreparams },},selectOption.headers);const myModifyData = arrayToTreeRec({data: resData.data.filter((el) => el.type !== -1),pid: 0,idKey: "pid",pidKey: "parentId",});data.resTreeData = resData.data;data.treeData = myModifyData;
};
const getListData = async (selectOption, moreparams) => {const { resData } = await useAxios({url: selectOption.url,method: selectOption.methed || "post",param: selectOption?.isNoGetParams ? "" : { ...moreparams },},selectOption.headers);let _topData = [];const _temArr = resData.list ? resData.list : resData.data;let _data = _temArr.map((el) => {el.label = el.name;el.value = el.pid;el.parentId = 0;if (selectOption.isPinxixi) {if (!el.shipCnName) {el.nameAndShipName = el.name;_topData.push(el);} else {el.nameAndShipName = el.name + "【" + el.shipCnName + "】";}}return el;});const _data2 = _data.filter((el) => el.shipCnName);const _data3 = [..._topData, ..._data2];const myModifyData = arrayToTreeRec({data: selectOption.isPinxixi ? _data3 : _data,pid: 0,idKey: selectOption.isPinxixi ? "name" : "pid",pidKey: "parentId",});data.treeDataList = myModifyData;
};const gettreeLocationSelect = async (selectOption, moreparams) => {if (!store.areaList) {const { resData } = await useAxios({url: selectOption.url,method: selectOption.methed || "post",param: selectOption?.isNoGetParams ? "" : { ...moreparams },},selectOption.headers);const _data = resData.list.map((el) => {el.label = el.name;el.value = el.pid;el.parentId = 0;if (selectOption.isPinxixi) {el.nameAndShipName = el.name + "【" + el.shipCnName + "】";}return el;});const myModifyData = arrayToTreeRec({data: _data,pid: 0,idKey: "pid",pidKey: "parentId",});data.treeLocationSelect = myModifyData;} else {const myModifyData = arrayToTreeRec({data: store.areaList.filter((el) => el.type !== -1),pid: 0,idKey: "pid",pidKey: "parentId",});data.treeLocationSelect = myModifyData;data.storeTrseeData = myModifyData;}
};
//切換省市地址
const citySelectChange = (value) => {data.formInline["province"] = value[0];data.formInline["city"] = value[1];
};// 自定義el-select組件的filter-method使得其可以編輯內容
const handleSelectEdit = (filterVal, item) => {if (item.filterable) data.editSelectValue = filterVal;
};
const filtertreeLocationSelect = async (filterVal, item, type) => {if (!filterVal) return;data.remoteSelectLoading = true;if (item.type == 'treeLocationSelect') {const _keyParam = item.filterablePrama.keyParamconst moreparams = { [_keyParam]: filterVal, ...item.filterablePrama };delete moreparams.keyParamawait handleSubFilter(item, moreparams);data.remoteSelectLoading = false;}
};
const handleSubFilter = async (selectOption, moreparams) => {const { resData } = await useAxios({url: selectOption.url,method: selectOption.methed || "post",param: selectOption?.isNoGetParams ? "" : { ...moreparams },},selectOption.headers);let myModifyDataconst _listArr = resData.list ? resData.list : resData.data;if (selectOption.isTrss) {//四川省樹狀結構const _data = _listArr.map((el) => {el.label = el.name;el.value = el.pid;if (selectOption.isPinxixi) {el.nameAndShipName = el.name + "【" + el.shipCnName + "】";}return el;});myModifyData = arrayToTreeRec({data: _data.filter((el) => el.type !== -1),pid: 0,idKey: "pid",pidKey: "parentId",});} else {myModifyData = _listArr}data.treeLocationSelect = myModifyData;
};
// onMounted(() => {
// getCtitysData();
// mapData = props.MapData;
// data.headerApi = { Authorization: store.userInfo.token }
// })
//獲取區域所屬省市
const getCtitysData = async () => {let requrl = "./json/city.json";let resData = await axios.get(requrl).then((res) => {return res.data.citiesData;});resData.map((item) => {item.name = item.province;return item;});data.citiesData = resData;
};
const setCheckBoxData = (item) => {setTimeout(() => {data.formInline[item.key] = item.optionCheckAll? ["0", ...item.optionsData.reduce((pre, cre) => [...pre, cre.value], [])]: [];data.checkedAll = data.formInline[item.key];}, 0);checkAll.value = true;isIndeterminate.value = false;
};//從服務器搜索下拉框內容
const remoteMethod = (query, selectItem) => {debounce(async () => {if (!query) return;data.remoteSelectLoading = true;const { resData } = await useAxios({url: selectItem.url,method: "post",param: { [selectItem.searchKey]: query },},selectItem.headers);const finalListData = selectItem.handleSpecialData? selectItem.handleSpecialData(resData.data): resData.data;// data.selectDataAllInfo = finalListData;if (finalListData.length) {data.remoteSelectData = finalListData.map((item) => {if (selectItem.labelMulti)return {label:item[selectItem.optionParam.label]?.trim() ||item[selectItem.optionParam.otherlable]?.trim(),value: item[selectItem.optionParam.value],itemInfo: {...selectItem.labelMulti,addDataInfo: item,},};return {label:item[selectItem.optionParam.label]?.trim() ||item[selectItem.optionParam.otherlable]?.trim(),value: item[selectItem.optionParam.value],itemInfo: JSON.stringify(item),};});} else {//避免重復插入多個let isAdd = data.remoteSelectData.filter((item) => item.value == query);if (!isAdd.length) {data.remoteSelectData.unshift({ label: query, value: query });}}data.remoteSelectLoading = false;setTimeout(() => {data.remoteSelectLoading = false;}, 1000);}, 500)();
};
const getRemoteSelect = async (selectOption, moreparams) => {if (props.dialogType == "edit") {selectOption.pageInfo = {[selectOption.searchKey]: props.formItemsVal[selectOption.searchKey],};}const { resData } = await useAxios({url: selectOption.url,method: "post",param: selectOption.pageInfo ?? { pageNo: 1, pageSize: 0, ...moreparams },},selectOption.headers);const finalListData = selectOption.handleSpecialData? selectOption.handleSpecialData(resData.data): resData.data;// data.selectDataAllInfo = finalListData;if (finalListData.length) {data.remoteSelectData = finalListData.map((item) => {if (selectOption.labelMulti)return {label:item[selectOption.optionParam.label]?.trim() ||item[selectOption.optionParam.otherlable]?.trim(),value: item[selectOption.optionParam.value],itemInfo: {...selectOption.labelMulti,addDataInfo: item,},};return {label:item[selectOption.optionParam.label]?.trim() ||item[selectOption.optionParam.otherlable]?.trim(),value: item[selectOption.optionParam.value],};});}
};const watchKeys = reactive([]);
watch(() => props.formItemsVal,(val) => {data.isAbled = false;if (val) {data.isAbled = true;data.formInline = {};data.formInline = Object.assign({}, val);}},{ immediate: true }
);
watch(() => props.formItems,(val) => {//構造需要動態顯示的表單下拉項數據val.forEach((item) => {if (item.key) {//某些值是否需要監視if (item.watchChage) {watchKeys.push(item);}//!props.formItemsVal代表添加if (!props.formItemsVal) {data.formInline[item.key] =item.defaultValue !== undefined && item.defaultValue !== null? item.defaultValue: "";}if (item.type == "select") {if (item.optionsData.length == 0 && item.url) {// 動態獲取下拉框內容getSelectData(item);} else if (item.defaultVal) {data.formInline[item.key] = item.defaultVal;} else if (item.defaultVal && !props.formItemsVal) {data.formInline[item.key] = item.defaultVal;}if (item.linkageKey) {if (props.formItemsVal) {data.formInline[item.key] = props.formItemsVal[item.key];searchChange(item.linkageKey,item.optionsData,data.formInline[item.key],item.linkageDraw);}}}if (item.type == "selectTree") {if (item.optionsData.length == 0 && item.url) {// 動態獲取下拉框內容getSelectTreeData(item);}}if (item.type == "treeMuiltSelect") {if (item.optionsData.length == 0 && item.url) {data.tempObj = item;getListData(item, item.moreparams);} else {data.treeDataList = item.optionsData;}}if (item.type == "treeLocationSelect") {if (item.optionsData.length == 0 && item.url) {data.tempObj = item;gettreeLocationSelect(item, item.moreparams);}}if (item.type == "textarea" && item.defaultVal)props.dialogType !== "edit" &&(data.formInline[item.key] = item.defaultVal);if (item.type == "checkBox") {data.formInline[item.key] = [];//item.optionCheckAll==true,默認填充一個全部類型的值為0setCheckBoxData(item);}if (item.type == "remoteSelect") {getRemoteSelect(item);}if (item.rule) {merge(data.rules, { [item.key]: item.rule });}// 聯動邏輯最終解決方案(優于之前的分散的聯動邏輯)finallyLinkageSolve(item);}});},{immediate: true,}
);let instance = null; //繪制地理要素
let drawLayer = null; //繪制時臨時創建的圖層
let mapData = {};
//初始化繪制
const drawArea = () => {initDraw();drawLayer && drawLayer.getSource().clear();instance = mapDrawGraph(mapData.MapData,drawLayer,"Point",handlerDarwResult);
};
const initDraw = () => {let _Map = mapData;instance && _Map.MapData.GlobalMap.removeInteraction(instance);drawLayer = _Map._layer.creatTempLayer({layerName: "drawTempPoint",title: "繪制臨時圖層選點",zindex: 10,});
};
const handlerDarwResult = (drawResult) => {if (drawResult.coods.length !== 0) {let myCo = Object.assign({}, drawResult.coods);data.coordinate = myCo[0];for (let key in data.coordinate) {data.coordinate[key].indexOf("") >= 0 &&(data.coordinate[key] = data.coordinate[key].trim());}}instance = null;
};
const handleOpenDialog = (row, item) => {item.getRowInfo({ ...row, ...item, ...data.formInline });
};
const uploadRef = ref();
const handleResult = (state, info, item) => {if (state === "success") {const { data: resData } = info;const { address, fileName } = resData;const _fileName = !fileName? "附件名稱" + resData.split("/")[resData.split("/").length - 1]: fileName;if (!fileName) {data.formInline[item.key] = resData;data.headerResult.fileName = resData;} else {data.formInline.fileName = _fileName;data.formInline.address = address;}item.getRowInfo({ ...resData, ...item, ...data.formInline });}ElMessage({type: state,message: `導入文件上傳${state === "success" ? "成功" : "失敗"}`,});
};
const fileClear = (state, info, item) => {if (state === "fileClear") {data.formInline[item.key] = "";const params = {fileName: info,fileType: item.option[0].uploadParames.fileType,};item.clearFunc(params);}
};
const uploadSingleImg = (state, info, item) => {if (state === "success") {const { data: resData } = info;item.getRowInfo(resData);upImgState.dialogImageUrl = BASEUrl + "/file/" + resData;const _fileList = data.formInline[item.key] || [];_fileList.unshift({ url: upImgState.dialogImageUrl });data.formInline[item.key] = _fileList;}ElMessage({type: state,message: `單個文件上傳${state === "success" ? "成功" : "失敗"}`,});
};
const beforeUploadSingleImg = (rawFile) => {if (!rawFile) return false;if (rawFile.type !== "image/jpeg" && rawFile.type !== "image/png") {ElMessage.error("文件類型須為圖片格式!");return false;} else if (rawFile.size / 1024 / 1024 > 10) {ElMessage.error("文件大小須小于10MB!");return false;}return true;
};
const beforeRemoveSingleImg = (state, info, item) => {if (state === "remove") {let _fileName = "";if (!info.response) {_fileName = info.url.split("/")[info.url.split("/").length - 1];} else {_fileName =info.response.data.split("/")[info.response.data.split("/").length - 1];}const _fileType = item.option.uploadParames.fileType;item.removeImg({title: "刪除圖片",fileName: _fileName,fileType: _fileType,});data.formInline[item.key] = [];}return true;
};
const upImgState = reactive({dialogVisible: false, //預覽彈框顯示與否dialogImageUrl: "",fileList: [],
});
const handlePictureCardPreview = (file) => {upImgState.dialogImageUrl = file.url || BASEUrl + file.url;upImgState.dialogVisible = true;
};
const handlechange = (val, limit, isJump) => {if (isJump) {let _data = {};let _val = val;if (Array.isArray(val)) {_val = _val[_val.length - 1];}_data = data.resTreeData.find((item) => item.pid == _val);if (!_data) return;locationPoint(_data.geom);}if (!limit) return;if (limit == "有聯動") {getListData(data.tempObj, {...data.tempObj.moreparams,areaIdCondition: val,});}if (limit == "有聯動2") {getListData(data.tempObj, {organization: val,});}
};
const handleSelectchange = (val, item) => {const _findObj = store.areaList.find((el) => el.pid == val[val.length - 1]);if (_findObj) {const isOk =Array.isArray(_findObj.ferryPortAreaVos) &&_findObj.ferryPortAreaVos.length > 0;if (!isOk) {data.formInline[item.key] = data.formInline[item.key].filter((el) => {return el !== val[val.length - 1];});ElMessage.warning("該區域下暫無渡口!");}}
};
const blur = (item, type) => {if (item.type === "treeLocationSelect") {zeroSelectData();}
};
const zeroSelectData = () => {data.treeLocationSelect = data.storeTrseeData
}
defineExpose({validateForm,resetForm,getFormItems,getFromData,getListData,
});
</script><style lang="scss" scoped>
.area-from {display: flex;justify-content: space-between;flex-wrap: wrap;:deep(.el-form-item) {margin-right: 0px;// margin-bottom: 20px;max-width: 50%;.el-form-item__label {color: #fff;font-size: 16px;padding: 0px;padding-right: 10px;min-width: 100px;display: flex;justify-content: flex-end;align-items: center;line-height: 22px;}.el-form-item__error {white-space: nowrap;}.el-input__wrapper {background: rgba(26, 136, 198, 0.3);box-shadow: none;border: 1px solid #62a3ff;border-radius: unset;.el-input__inner {color: #fff;}.el-input__inner::placeholder {color: #d0eeff;}.el-input__suffix {color: #fff;}}.el-select__wrapper {background-color: #08387e;box-shadow: 0 0 0 1px #62a3ff inset;}.el-select__placeholder {color: #fff;}.el-radio__label {color: #fff;}.el-select .el-input__wrapper.is-focus {box-shadow: unset !important;}.el-input--suffix {color: #d0eeff !important;}.unit {color: #fff;margin-left: 5px;}.el-radio-group {min-width: 300px;}}.ship-white-warn {max-width: 100% !important;.warn-type {display: flex;align-items: center;:deep(.el-checkbox-group) {margin-left: 10px;}}}.select-area {width: 100%;>.el-form-item {width: 100%;max-width: 100%;margin-right: 0;}:deep(.select-trigger) {height: 100%;.el-input {height: 100%;}}:deep(.el-select__tags .el-tag--info) {background-color: rgb(16, 159, 204) !important;color: #ffffff;height: 30px;}:deep(.el-tag .el-tag__close) {color: #ffffff;}}.area-remark {// margin-top: 10px;width: 100%;>.el-form-item {width: 100%;max-width: 100%;margin-right: 0;.el-textarea {// width: 91%;:deep(.el-textarea__inner) {background-color: rgba(26, 136, 198, 0.3);border: 1px solid #62a3ff;border-radius: unset;outline: none;box-shadow: unset;color: #ffffff;&::placeholder {color: #d0eeff;}}}}}.warning-level {display: flex;justify-content: space-between;width: 100%;}.warning-distance {width: 100%;display: flex;justify-content: space-between;:deep(.el-form-item) {.el-form-item__content {flex-wrap: nowrap;}}}.cordinate-container {display: flex;align-items: center;:deep(.el-form-item) {max-width: 100%;margin-right: 0;.el-form-item__content {flex-wrap: nowrap;}}.wrapper {// width: 80%;background: rgba(26, 136, 198, 0.3) !important;border: 1px solid #62a3ff;padding: 6px 16px;display: flex;// flex-wrap: nowrap;.row-input-box {display: flex;align-items: center;&:first-child {margin-right: 22px;}.row-name {margin-right: 8px;white-space: nowrap;font-size: 14px;font-family: Microsoft YaHei-Regular, Microsoft YaHei;font-weight: 400;color: #d0eeff;line-height: 22px;}.input-item {display: flex;align-items: center;.inp-unit {margin: 0 6px;font-size: 12px;font-family: Microsoft YaHei-Regular, Microsoft YaHei;font-weight: 400;color: #52cbff;line-height: 19px;}.el-input {// width: 50px;:deep(.el-input__wrapper) {border: unset;border-radius: unset;background: #074665;padding: 1px;>input {text-align: center;}}}}}}.box {cursor: pointer;margin-left: 5px;display: inherit;align-items: inherit;}}.psw_home {padding-bottom: 15px;}.opration-box,.render-box {display: flex;justify-content: center;span {margin-right: 5px;}img {width: 16px;}}:deep(.el-input-group__append) {border-left: 0;border-top-left-radius: 0;border-bottom-left-radius: 0;box-shadow: 0 1px 0 0 #62a3ff inset, 0 -1px 0 0 #62a3ff inset,-1px 0 0 0 #62a3ff inset;background-color: #075480;padding: 0px 10px;background-color: #095680;}.avatar-uploader {width: 50px;height: 50px;display: flex;justify-content: center;align-items: center;border: 1px dashed #d9d9d9;img {width: 98%;height: 98%;object-fit: cover;}}
}
</style>
<style lang="scss">
.custom-header {.el-checkbox {display: flex;height: unset;}// .el-select-dropdown.is-multiple .el-select-dropdown__item.selected {// color: #606266 !important;// }.el-select-dropdown.is-multiple .el-select-dropdown__item.selected::after {// background-color: rgb(47, 255, 127) !important;width: 25px;height: 25px;font-weight: bolder;font-size: 16px;}
}
</style>
調用組件AreaFormItem.vue的方式:
//調用組件
<template><AreaFormItemref="areaForm":formItems="formItems":formItemsVal="state.formItemsVal"/>
</template><script setup>
import { nextTick, onMounted, reactive, ref, watch } from "vue";
import AreaFormItem from "@/components/Form/AreaFormItem.vue";
const state = reactive({formItemsVal: {},
});
//備注:state.formItemsVal用于表單回顯
const formItems = [{type: "datePicker",label: "簽發日期",key: "dateOfIssue",rule: [{required: true,message: "簽發日期不能為空!",trigger: "blur",},],},{type: "datePicker",label: "截止日期",key: "deadline",rule: [{required: true,message: "截止日期不能為空!",trigger: "blur",},],},{type: "select",label: "文件類型",key: "type",optionsData: [{label: "船舶經營許可證",value: 0,},{label: "船舶管理人許可證",value: 7,},],defaultVal: 0,rule: [{required: true,message: "文件類型不能為空!",trigger: "blur",},],},{type: "uploadSingleImg",label: "渡船證書",key: "file",option: {uploadParames: {fileType: 0, //渡船相關證書},img_bg: "table/upload.png",info: "upload",text: "上傳",importUrl: BASEUrl + "/data/file/upload",},width: "100%",getRowInfo: (data) => {console.log("上傳圖片的結果", data);state.formItemsVal.file = data;},removeImg: (data) => {api.fileApi.deleteFile({ fileName: data.fileName, fileType: data.fileType }).then((res) => {if (res.status === 200 && res.data.code === 200) {ElMessage({type: "success",message: "刪除成功",});}}).catch((err) => {console.log(err);});},rule: [{required: true,message: `${state.pageTheme}不能為空!`,trigger: "blur",},],},
];
<script>
?寫到這兒就實現了表單組件的封裝,根據調用組件傳參的formItems變量的type類型渲染不同的表單組件例如:input、select等組件。。。最終隨著項目的進行,表單組件的其他類型:
'location',?'medium','password',?'sexRadio',?'coordinate','radio','textarea','multipleSelect',?'uploadFile','uploadSingleImg',也會封裝。