<!-- filename: dialogForm.vue author: liubin date: 2023-08-15 10:32:36 description: 弹窗的表单组件 --> <template> <el-form ref="form" :model="form" :label-width="`${labelWidth}px`" :size="size" :label-position="labelPosition" v-loading="formLoading"> <el-row :gutter="20" v-for="(row, rindex) in rows" :key="rindex"> <el-col v-for="col in row" :key="col.label" :span="24 / row.length"> <el-form-item :label="col.label" :prop="col.prop" :rules="col.rules"> <el-input v-if="col.input" v-model="form[col.prop]" @change="$emit('update', form)" :placeholder="`请输入${col.label}`" :disabled="disabled" v-bind="col.bind" /> <el-input v-if="col.textarea" type="textarea" v-model="form[col.prop]" :disabled="disabled" @change="$emit('update', form)" :placeholder="`请输入${col.label}`" v-bind="col.bind" /> <el-select v-if="col.select" v-model="form[col.prop]" :placeholder="`请选择${col.label}`" :disabled="disabled" @change="$emit('update', form)" v-bind="col.bind"> <el-option v-for="opt in optionListOf[col.prop]" :key="opt.value" :label="opt.label" :value="opt.value" /> </el-select> <el-date-picker v-if="col.datetime" v-model="form[col.prop]" type="datetime" :disabled="col.disabled ? col.disabled : disabled" :placeholder="`请选择${col.label}`" value-format="timestamp" @change="$emit('update', form)" v-bind="col.bind"> </el-date-picker> <el-switch v-if="col.switch" v-model="form[col.prop]" :disabled="disabled" active-color="#0b58ff" inactive-color="#e1e1e1" @change="$emit('update', form)" v-bind="col.bind"></el-switch> <component v-if="col.subcomponent" :key="col.key" :disabled="disabled" :read-only="disabled" :is="col.subcomponent" v-model="form[col.prop]" :inlineStyle="col.style" @on-change="$emit('update', form)" v-bind="col.bind"></component> <div class="upload-area" :class="uploadOpen ? '' : 'height-48'" ref="uploadArea" :key="col.prop" v-if="col.upload"> <span class="close-icon" :class="uploadOpen ? 'open' : ''"> <el-button type="text" icon="el-icon-arrow-right" @click="handleFilesOpen" /> </span> <!-- :file-list="uploadedFileList" --> <el-upload class="upload-in-dialog" v-if="col.upload" :key="col.prop + '__el-upload'" :action="uploadUrl" :headers="uploadHeaders" :show-file-list="false" icon="el-icon-upload2" :disabled="disabled" :before-upload="beforeUpload" :on-success=" (response, file, fileList) => { handleUploadSuccess(response, file, col.prop); } " v-bind="col.bind"> <el-button size="mini" :disabled="col.bind?.disabled || false"> <svg-icon icon-class="icon-upload" style="color: inherit"></svg-icon> 上传文件 </el-button> <div class="el-upload__tip" slot="tip" v-if="col.uploadTips"> {{ col.uploadTips || '只能上传jpg/png文件, 大小不超过2MB' }} </div> </el-upload> <uploadedFile class="file" v-for="file in form[col.prop]" :file="file" :key="file.fileUrl" @delete="!disabled && handleDeleteFile(file, col.prop)" /> </div> </el-form-item> </el-col> </el-row> </el-form> </template> <script> import { getAccessToken } from '@/utils/auth'; import tupleImg from '@/assets/images/tuple.png'; import cache from '@/utils/cache'; /** * 找到最长的label * @param {*} options */ function findMaxLabelWidth(rows) { let max = 0; rows.forEach((row) => { row.forEach((opt) => { // debugger; if (!opt.label) return 0; if (opt.label.length > max) { max = opt.label.length; } }); }); return max; } const uploadedFile = { name: 'UploadedFile', props: ['file'], data() { return {}; }, methods: { handleDelete() { this.$emit('delete', this.file); }, async handleDownload() { const data = await this.$axios({ url: this.file.fileUrl, method: 'get', responseType: 'blob', }); await this.$message.success('开始下载'); // create download link const url = window.URL.createObjectURL(data); const link = document.createElement('a'); link.href = url; link.download = this.file.fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); }, }, mounted() {}, render: function (h) { return ( <div title={this.file.fileName} onClick={this.handleDownload} style={{ background: `url(${tupleImg}) no-repeat`, backgroundSize: '14px', backgroundPosition: '0 55%', paddingLeft: '20px', paddingRight: '24px', textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden', cursor: 'pointer', display: 'inline-block', }}> {this.file.fileName} <el-button type="text" icon="el-icon-close" style="float: right; position: relative; top: 2px; left: 8px; z-index: 100" class="dialog__upload_component__close" onClick={this.handleDelete} /> </div> ); }, }; export default { name: 'DialogForm', model: { prop: 'dataForm', event: 'update', }, emits: ['update'], components: { uploadedFile }, props: { rows: { type: Array, default: () => [], }, dataForm: { type: Object, default: () => ({}), }, disabled: { type: Boolean, default: false, }, hasFiles: { type: Boolean | Array, default: false, }, labelPosition: { type: String, default: 'right', }, size: { type: String, default: '', }, }, data() { return { uploadOpen: false, form: {}, formLoading: true, optionListOf: {}, uploadedFileList: [], dataLoaded: false, uploadHeaders: { Authorization: 'Bearer ' + getAccessToken() }, uploadUrl: process.env.VUE_APP_BASE_API + '/admin-api/infra/file/upload', // 上传有关的headers,url都是固定的 }; }, computed: { labelWidth() { let max = findMaxLabelWidth(this.rows); // 每个汉字占20px return max * 20; // return max * 20 + 'px'; }, }, watch: { rows: { handler() { this.$nextTick(() => { this.handleOptions('watch'); }); }, deep: true, immediate: false, }, dataForm: { handler(val) { this.form = JSON.parse(JSON.stringify(val)); if (this.hasFiles) { if (typeof this.hasFiles == 'boolean' && this.hasFiles) { this.form.files = this.form.files ?? []; } else if (Array.isArray(this.hasFiles)) { this.hasFiles.forEach((prop) => { this.form[prop] = this.form[prop] ?? []; }); } } }, deep: true, immediate: true, }, }, mounted() { // 处理 options this.handleOptions(); }, methods: { /** 模拟透传 ref */ validate(cb) { return this.$refs.form.validate(cb); }, resetFields(args) { return this.$refs.form.resetFields(args); }, // getCode async getCode(url) { const response = await this.$axios(url); return response.data; }, async handleOptions(trigger = 'monuted') { console.log('[dialogForm:handleOptions]'); const promiseList = []; this.rows.forEach((cols) => { cols.forEach((opt) => { if (opt.value && !this.form[opt.prop]) { // 默认值 this.form[opt.prop] = opt.value; } if (opt.options) { this.$set(this.optionListOf, opt.prop, opt.options); } else if (opt.url) { // 如果有 depends,则暂时先不获取,注册一个watcher if (opt.depends) { this.$watch( () => this.form[opt.depends], (id) => { console.log('<', opt.depends, '>', 'changed', id); if (id == null) return; // 清空原有选项 this.form[opt.prop] = null; // 获取新的选项 this.$axios({ url: `${opt.url}?id=${id}`, }).then((res) => { this.$set( this.optionListOf, opt.prop, res.data.map((item) => ({ label: item[opt.labelKey ?? 'name'], value: item[opt.valueKey ?? 'id'], })) ); }); }, { immediate: false, } ); return; } // 如果是下拉框,或者新增模式下的输入框,才去请求 if (opt.select || (opt.input && !this.form?.id)) { promiseList.push(async () => { const response = await this.$axios(opt.url, { method: opt.method ?? 'get', }); console.log('[dialogForm:handleOptions:response]', response); if (opt.select) { // 处理下拉框选项 const list = 'list' in response.data ? response.data.list : response.data; if (opt.cache) { cache.store(opt.cache, list); } this.$set( this.optionListOf, opt.prop, list.map((item) => ({ label: item[opt.labelKey ?? 'name'], value: item[opt.valueKey ?? 'id'], })) ); } else if (opt.input) { console.log('setting code: ', response.data); // 处理输入框数据 this.form[opt.prop] = response.data; // 更新下外部的 dataForm,防止code字段有数据也报空的bug this.$emit('update', this.form); } }); } } }); }); console.log('[dialogForm:handleOptions] done!'); // 如果是 watch 触发的,不需要执行进一步的请求 if (trigger == 'watch') { this.formLoading = false; return; } try { await Promise.all(promiseList.map((fn) => fn())); this.formLoading = false; this.dataLoaded = true; // console.log("[dialogForm:handleOptions:optionListOf]", this.optionListOf) } catch (error) { console.log('[dialogForm:handleOptions:error]', error); this.formLoading = false; } if (!promiseList.length) this.formLoading = false; }, // 上传成功的特殊处理 beforeUpload() {}, // 上传前的验证规则可通过 bind 属性传入 handleUploadSuccess(response, file, prop) { console.log('[handleUploadSuccess]', response, file, prop); this.form[prop].push({ fileName: file.name, fileUrl: response.data, fileType: prop == 'files' ? 2 : 1, }); this.$modal.msgSuccess('上传成功'); this.$emit('update', this.form); }, getFileName(fileUrl) { return fileUrl.split('/').pop(); }, handleFilesOpen() { this.uploadOpen = !this.uploadOpen; }, handleDeleteFile(file, prop) { this.form[prop] = this.form[prop].filter( (item) => item.fileUrl != file.fileUrl ); this.$emit('update', this.form); }, }, }; </script> <style scoped lang="scss"> .el-date-editor, .el-select { width: 100%; } .upload-area { // background: #ccc; // display: grid; // grid-auto-rows: 34px; // grid-template-columns: repeat(6, minmax(32px, max-content)); // gap: 8px; // align-items: center; position: relative; overflow: hidden; transition: height 0.3s ease-out; } .upload-in-dialog { // display: inline-block; margin-right: 24px; // background: #ccc; position: relative; // top: -13px; float: left; } .close-icon { // background: #ccc; position: absolute; top: 0; right: 12px; z-index: 100; transition: transform 0.3s ease-out; } .close-icon.open { transform: rotateZ(90deg); } </style> <style> .dialog__upload_component__close { color: #ccc; } .dialog__upload_component__close:hover { /* color: #777; */ color: red; } .height-48 { height: 35px !important; } </style>