vue2企业级项目(七)
组件封装(一)
-
src
目录下创建components/index.js
const components = require.context("./modules", true, /index\.js$/); export default { install: (Vue) => { components.keys().forEach((key) => { let component = components(key).default; component && Vue.component(component.name, component); }); }, };
-
创建
src/components/modules
目录
1、form
-
modules
下创建modules/x-form/index.js
import XForm from "./index.vue"; export default XForm;
-
创建
modules/x-form/index.vue
<script> import { cloneDeep } from "lodash"; import { getDefaultValue, getPlaceholder, getRules, createInput, } from "../../createInput.js"; /** * 配置说明文档 * @param { Array } columns: 表单配置项 * @param { String } labelWidth: 设置el-form-item的label宽度。(优先级低于item) * @param { Number } contentSpan: 设置el-form-item的内容占比宽度 * @param { Object } defaultValue: 设置el-form的默认值。(级别最高) * @param { Object } rules: 设置el-item的校验规则。(优先级低于item) * @param { Number } gutter: 设置el-row的gutter * @param { Number, Array } grid: 设置el-col的span,如果没有,则不创建el-col * @param { Boolean } disabled: 表单统一仅用。(优先级低于item) * * @param { Boolean } ctrl: 是否显示表单控制按钮 * @param { String } ctrlSpan: 表单控制器占比,默认跟随grid * @param { Boolean } submitBtn: 是否显示提交按钮 * @param { String } submitText: 提交按钮内容 * @param { String } submitIcon: 提交按钮icon * @param { Boolean } submitLoading: 提交按钮的loading开启 * @param { Boolean } resetBtn: 是否显示重置按钮 * @param { String } resetText: 重置按钮内容 * @param { String } resetIcon: 重置按钮icon * @param { Boolean } resetLoading: 重置按钮的loading开启 * * @event { Function } submit: 表单提交方法,返回表单值和校验结果 * @event { Function } reset: 表单重置方法 * @event { Function } clear: 表单清空方法 * @event { Function } getFormByKey: 获取表单单值方法 * @event { Function } getForm: 获取表单全值方法 * @event { Function } setForm: 设置表单单值方法 */ /** * 表单配置项 columns-item * @param { Object } form: 单独作用form的配置 * @param { Object } table: 单独作用table的配置 * @param { Object } search: 单独作用search的配置 * 以上的配置和下面一样,但优先级高 * * * @param { String } type: 表单控件的类型。(参考element-ui的控件类型) * @param { String } label: 字段名称 * @param { String } prop: 字段绑定key * @param { Any } default: 控件默认值。(无法被重置) * @param { String } placeholder: 提示信息 * @param { Array } rules: 校验规则。(级别最高) * @param { Number } order: 排序序号。(大到小) * @param { Number } span: 当前表单控件col占比。(级别高) * @param { Object } col: el-col的其他配置。(级别最高) * @param { String } labelWidth: label的宽度。(级别最高) * @param { Number } contentSpan: 内容占比宽度 * @param { Boolean } required: 是否必填 * @param { Boolean } disabled: 是否禁用。(权限最高) * * @param { Object } children: 子节点, (一个el-form-item里面多个控件) * @param { Number } gutter: 子节点的el-row的gutter。(仅在设置children后生效) * @param { Number, Array } grid: 子节点的栅格布局col排布 * * @param { Object } className: 表单控件的类名,对象的形式:{ box: true } * @param { Object } style: 样式控制 * @param { Object } attrs: element-ui表单控件的attribute,参考element-ui文档。 * @param { Object } props: element-ui表单控件的props。(有的时候文档上写的时attrs设置,其实时props设置。所以不生效的时候可以尝试一下) * @param { Object } on: 表单控件的绑定事件。{ input: (value) => console.log(value) } * @param { Array } options: radio,checkbox, select, cascader, * @param { String } text: button按钮的文字内容 * @param { String } btnType: button按钮的类型 * @param { String } btnIcon: button的icon * @param { String } marking: button的标记 * @param { Boolean } hasFilter: 是否进行筛选,只有存在options才可以进行 * * @param { Function } slotDefault: el-table-column 的 default的插槽 * @param { Function } slotHeader: el-table-column 的 header的插槽 */ export default { name: "XForm", props: { labelWidth: String, columns: { type: Array, default: () => [], }, defaultValue: { type: Object, default: () => ({}), }, contentSpan: { type: Number, default: 24, }, rules: { type: Object, default: () => ({}), }, gutter: { type: Number, default: 0, }, grid: [Number, Array], disabled: Boolean, ctrl: { type: [Boolean, String], default: "left", }, ctrlSpan: { type: Number, default: null, }, submitBtn: { type: Boolean, default: true, }, submitText: { type: String, default: "提交", }, submitIcon: { type: String, default: "", }, submitLoading: { type: Boolean, default: false, }, resetBtn: { type: Boolean, default: true, }, resetText: { type: String, default: "重置", }, resetIcon: { type: String, default: "", }, resetLoading: { type: Boolean, default: false, }, }, render(h) { return h( "el-form", { attrs: { ...this.$attrs }, props: { model: this.form, }, style: { padding: `0 ${this.gutter / 2}px`, }, ref: "form", nativeOn: { submit(e) { e.preventDefault(); e.stopPropagation(); }, }, }, [this.renderRow(h, this.fields, this.gutter), this.$slots.default], ); }, data() { return { form: {}, fields: [], sLoading: false, rLoading: false, }; }, watch: { columns: { deep: true, handler() { this.init(); }, }, defaultValue: { deep: true, handler(value) { this.form = cloneDeep(value); this.init(); }, }, }, created() { this.form = cloneDeep(this.defaultValue); this.init(); }, methods: { init() { let form = cloneDeep(this.form); // 获取配置属性 this.fields = this.handleFieldsChild(form, this.columns); // 添加校验按钮 if (this.ctrl) { let _this = this; this.fields.push({ span: _this.ctrlSpan, render: ({ h }) => { return h( "div", { class: { formBtns: true }, style: { textAlign: this.ctrl, }, }, [ _this.submitBtn && createInput( h, { type: "button", btnType: "primary", text: _this.submitText, on: { click: _this.submit, }, props: { icon: _this.submitIcon, loading: _this.sLoading, disabled: _this.disabled, }, }, _this, ), _this.resetBtn && createInput( h, { type: "button", text: _this.resetText, on: { click: _this.reset, }, props: { icon: _this.resetIcon, loading: _this.rLoading, disabled: _this.disabled, }, }, _this, ), ], ); }, }); } this.form = { ...form }; }, handleFieldsChild(form, columns, parent = null) { let fields = columns.filter((item) => item?.isShow !== false); fields = fields.map((item) => { let formItem = { ...item, ...item?.form }; delete formItem.table; delete formItem.search; delete formItem.form; delete formItem.dialog; if (formItem.children && Array.isArray(item.children)) { formItem.children = this.handleFieldsChild( form, formItem.children, formItem, ); } if (parent && !formItem.label) formItem.label = parent?.label; getDefaultValue(form, formItem); getPlaceholder(formItem); getRules(formItem, this.rules); if (parent) { formItem.grid = parent?.grid; delete formItem.label; } return formItem; }); fields = fields.sort((a, b) => (b?.order || 0) - (a?.order || 0)); return fields; }, renderRow(h, fields, gutter, isChild = false) { let style = null; if (isChild) { style = { position: "relative", left: `-${gutter / 2}px`, marginLeft: "0", marginRight: "0", width: `calc(100% + ${gutter}px)`, }; } return h( "el-row", { attrs: { gutter }, style }, this.renderItem(h, fields, isChild), ); }, renderItem(h, fields, isChild) { return fields.map((item, index) => this.renderCol(h, { ...item, isChild }, index), ); }, renderCol(h, item, index) { delete item.order; const gridConfig = item.grid || this.grid; let grid = 0; if (typeof gridConfig === "number") grid = gridConfig; if (Array.isArray(gridConfig)) grid = this.grid[index % gridConfig?.length]; let span = item.span || item?.col?.span || grid; if (span) { return h( "el-col", { class: { "form-item-col": true }, attrs: { span, ...item.col }, }, [this.renderFormItem(h, item)], ); } else { return this.renderFormItem(h, item); } }, renderFormItem(h, item) { let labelWidth = item?.labelWidth || this.labelWidth || "0px"; let isChild = item.isChild; let hasChild = Array.isArray(item.children); if (isChild) labelWidth = "0px"; delete item?.labelWidth; delete item?.isChild; let childDom = null; if (hasChild) { const gutter = item?.gutter || this.gutter; const children = item?.children || []; childDom = this.renderRow(h, children, gutter, true); } else { childDom = h( "el-col", { attrs: { span: isChild ? 24 : item.contentSpan || this.contentSpan, }, }, [createInput(h, { ...item }, this)], ); } delete item?.children; delete item?.gutter; let itemRender = null; if (item.render && typeof item.render === "function") { itemRender = item.render({ h: this.getHypeScript(), value: this.form[item.prop], item, _this: this, }); } delete item.render; return h( "el-form-item", { style: { marginBottom: hasChild ? "0px" : "22px" }, attrs: { ...item, labelWidth }, }, [childDom, itemRender], ); }, // 表单方法 getHypeScript() { return this.$parent.$createElement; }, submit() { return new Promise((resolve) => { this.$refs.form.validate((valid) => { if (this.submitLoading && valid) this.sLoading = true; const _this = this; const callback = () => { _this.sLoading = false; }; this.$emit("submit", this.getForm(), valid, callback); resolve({ form: this.getForm(), valid, callback }); }); }); }, reset() { return new Promise((resolve) => { this.$refs.form.resetFields && this.$refs.form.resetFields(); this.form = {}; this.init(); if (this.resetLoading) this.rLoading = true; const callback = () => { this.rLoading = false; }; this.$emit("reset", callback); resolve(callback); }); }, clear() { this.$refs.form.clearValidate && this.$refs.form.clearValidate(); }, getFormByKey(key) { return this.form[key]; }, getForm() { return { ...this.form }; }, setForm(key, value) { key && (this.form[key] = value); }, }, }; </script> <style lang="less" scoped> .el-readonly-input { .el-input__inner { border: none; } .el-input__inner:focus { border-color: #dcdfe6; } } .form-item-col { .el-form-item { margin-bottom: 22px; } } </style>
-
src/components
创建createInput.js
import { isEqual } from "lodash"; export const tagsMenu = new Map([ ["text", "el-input"], ["xnumber", "x-input-number"], ["button", "el-button"], ["input", "el-input"], ["password", "el-input"], ["textarea", "el-input"], ["number", "el-input-number"], ["radio", "el-radio-group"], ["radio-item", "el-radio"], ["radio-button", "el-radio-button"], ["checkbox", "el-checkbox-group"], ["checkbox-item", "el-checkbox"], ["checkbox-button", "el-checkbox-button"], ["select", "el-select"], ["select-group", "el-option-group"], ["select-item", "el-option"], ["cascader", "el-cascader"], ["switch", "el-switch"], ["slider", "el-slider"], ["time-select", "el-time-select"], ["time", "el-time-picker"], ["date", "el-date-picker"], ["rate", "el-rate"], ["color", "el-color-picker"], ["transfer", "el-transfer"], ]); export const defaultValuesMenu = new Map([ ["radio", ""], ["checkbox", []], ["text", ""], ["input", ""], ["password", ""], ["textarea", ""], ["number", ""], ["xnumber", ""], ["select", null], ["cascader", null], ["switch", ""], ["slider", 0], ["time-select", ""], ["time", ""], ["date", ""], ["rate", null], ["color", null], ["transfer", []], ]); export function getDefaultValue(form, defaultValue = {}, item) { if (!item.prop) return; let value = defaultValuesMenu.get(item.type); if (item?.props?.multiple || item?.props?.isRange) value = []; form[item.prop] = form[item.prop] || item.default || defaultValue[item.prop] || value; } export function getPlaceholder(item) { const inputTypes = ["input", "password", "textarea", "number", "xnumber"]; const selectType = ["select", "cascader", "time-select", "time", "date"]; const dateRangeTypes = ["datetimerange", "daterange", "monthrange"]; item.attrs = item.attrs || {}; item.props = item.props || {}; // 添加date-range的标识 if (item.props?.type && dateRangeTypes.includes(item.props.type)) { item.props.isRange = true; } if (item.placeholder) { item.attrs.placeholder = item.placeholder; } else { if (inputTypes.includes(item.type)) item.attrs.placeholder = `请输入${item.label}`; if (selectType.includes(item.type)) item.attrs.placeholder = `请选择${item.label}`; if (item.props.isRange) { item.props.startPlaceholder = `请选择开始${item.label}`; item.props.endPlaceholder = `请选择结束${item.label}`; } } return item; } export function getRules(item, rules) { if (item.rules) return item; else if (rules && rules[item.prop]) { item.rules = rules[item.prop]; } else if (item.required) { item.rules = [ { required: true, message: item.attrs.placeholder || item.props.startPlaceholder || item.props.endPlaceholder, trigger: "blur", }, ]; } return item; } export function createInput(h, item, _this) { item.attrs = { type: item.type, options: item.type === "cascader" ? item.options : null, ...item.attrs, }; item.props = { disabled: item.disabled !== undefined ? item.disabled : _this.disabled, ...item.props, }; if (item.btnType) item.attrs.type = item.btnType; if (item.btnIcon) item.attrs.icon = item.btnIcon; if (item.marking) item.attrs.marking = item.marking; item.style = { width: item.type !== "button" ? "100%" : null, ...item.style, }; let { type, prop, className = {}, style = {}, attrs = {}, props = {}, on = {}, text = "", } = item; const config = { type, prop, class: className, style, attrs, props, on, text, children: generateOptions(h, item) || [], }; if (type === "time-select" || type === "date") { let minTimeKey = props?.pickerOptions?.minTimeKey; let maxTimeKey = props?.pickerOptions?.maxTimeKey; if (minTimeKey) { if (type === "time-select") { config.props = { ...config.props, pickerOptions: { ...config.props.pickerOptions, minTime: _this.form[minTimeKey], }, }; } else { let disabledDate = config.props.pickerOptions.disabledDate; if (typeof disabledDate === "function") { config.props = { ...config.props, pickerOptions: { ...config.props.pickerOptions, disabledDate: (time) => disabledDate(time, _this.form[minTimeKey]), }, }; } } } if (maxTimeKey) { if (type === "time-select") { config.props = { ...config.props, pickerOptions: { ...config.props.pickerOptions, maxTime: _this.form[maxTimeKey], }, }; } else { let disabledDate = config.props.pickerOptions.disabledDate; if (typeof disabledDate === "function") { config.props = { ...config.props, pickerOptions: { ...config.props.pickerOptions, disabledDate: (time) => disabledDate(time, _this.form[maxTimeKey]), }, }; } } } } if (type === "text") { config.class["el-readonly-input"] = true; config.attrs.readonly = true; } return generateTag(h, config, _this); } export function generateOptions(h, item) { const canTag = ["radio", "checkbox", "select"]; if (!canTag.includes(item.type)) return null; // 后续需要添加字典 const options = item?.options || []; return (options || []).map((option) => { let type = `${item.type}-${item.button ? "button" : "item"}`; if (option.options) { const cloneItem = { ...item }; cloneItem.options = option.options; type = `${item.type}-group`; return h( tagsMenu.get(type), { attrs: { label: option.label, }, }, generateOptions(h, cloneItem), ); } return h( tagsMenu.get(type), { attrs: { label: item.type === "select" ? option.label : option.value, value: item.labelInValue ? option : option.value, }, }, [item.type === "select" ? null : [option.label]], ); }); } export function generateTag(h, config, _this) { config.props.value = _this.form[config.prop]; const inputEvent = config.on?.input; const clickEvent = config.on?.click; config.on = { ...config.on, input: (value) => { value = formatDateValue(value, config); if (!isEqual(_this.form[config.prop], value)) { _this.form[config.prop] = value; typeof inputEvent === "function" && inputEvent(value, config); } }, click: (event) => { typeof clickEvent === "function" && clickEvent(event, config); }, }; config.nativeOn = { keydown: (e) => { if (e.keyCode === 13 && this.enterSubmit && config.type !== "textarea") { _this.submit(); } }, }; return h(tagsMenu.get(config.type), config, [ ...config.children, config.text, ]); } export function formatDateValue(value, config) { if ((config.type === "time" || config.type === "date") && !value) { return config.props.isRange ? ["", ""] : ""; } return value; }
-
使用案例
<template> <div> <!-- 基础表单 --> <hr /> <x-form :columns="columns1" labelWidth="100px" :ctrl="false"></x-form> <!-- 禁用表单 --> <hr /> <x-form :columns="columns1" labelWidth="100px" :ctrl="false" disabled ></x-form> <!-- 表单布局 --> <hr /> <x-form :columns="columns2" labelWidth="100px" :ctrl="false" :gutter="10" :grid="8" label-suffix=":" ></x-form> <!-- 灵活表单布局 --> <hr /> <x-form :columns="columns3" labelWidth="100px" :gutter="8" label-suffix=":" :contentSpan="20" :submitLoading="true" @submit="handleSubmit" ></x-form> <!-- 自定义表单元素 --> <hr /> <x-form :columns="columns4" :submitLoading="true" labelWidth="100px" @submit="handleSubmit" ></x-form> </div> </template> <script> import { cloneDeep } from "lodash"; export default { data() { return { columns1: [ { type: "text", label: "标题", prop: "title", default: "基础表单" }, { type: "input", label: "用户名", prop: "username" }, { type: "password", label: "密码", prop: "password" }, { type: "textarea", label: "信息", prop: "info", disabled: false }, ], columns2: [ { type: "text", label: "标题", prop: "title", default: "表单布局", span: 24, }, { type: "input", label: "输入1", prop: "value1" }, { type: "input", label: "输入2", prop: "value2" }, { type: "input", label: "输入3", prop: "value3" }, { type: "input", label: "输入4", prop: "value4" }, { type: "input", label: "输入5", prop: "value5" }, { type: "input", label: "输入6", prop: "value6" }, ], columns3: [ { type: "text", label: "标题", prop: "title", default: "灵活表单" }, { type: "input", label: "输入1", prop: "value1", }, { type: "input", label: "输入2", prop: "value2", required: true, }, { label: "输入3", required: true, children: [ { type: "input", prop: "value31", required: true, span: 10 }, { type: "input", prop: "value32", required: true, span: 10 }, { type: "input", prop: "value33", required: true, span: 10 }, { type: "input", prop: "value34", required: true, span: 10 }, { type: "input", prop: "value35", required: true, span: 10 }, { type: "input", prop: "value36", required: true, span: 10 }, ], }, { type: "input", label: "输入4", prop: "value4", required: true, }, { type: "input", label: "输入5", prop: "value5", contentSpan: 24, required: true, }, { type: "input", label: "输入6", prop: "value6", required: true, }, { label: "时间选择", gutter: 10, required: true, children: [ { type: "time-select", label: "开始时间", prop: "startTime", required: true, span: 10, props: { pickerOptions: { start: "08:30", step: "00:15", end: "18:30", maxTimeKey: "endTime", }, }, }, { type: "time-select", label: "结束时间", prop: "endTime", required: true, span: 10, props: { pickerOptions: { start: "08:30", step: "00:15", end: "18:30", minTimeKey: "startTime", }, }, }, ], }, { label: "日期选择", gutter: 10, required: true, children: [ { type: "date", label: "开始日期", prop: "startDate", required: true, span: 10, props: { pickerOptions: { maxTimeKey: "endDate", disabledDate: (time, date) => { if (!date) return false; return time.getTime() > new Date(date).getTime(); }, }, }, }, { type: "date", label: "结束日期", prop: "endDate", required: true, span: 10, props: { pickerOptions: { minTimeKey: "startDate", disabledDate: (time, date) => { if (!date) return false; return time.getTime() < new Date(date).getTime(); }, }, }, }, ], }, { label: "动态1", gutter: 10, marking: "1", children: [ { type: "input", prop: "active1", span: 20 }, { type: "button", btnType: "primary", btnIcon: "el-icon-plus", span: 2, marking: "1", on: { click: this.addForm, }, }, { type: "button", btnIcon: "el-icon-delete-solid", span: 2, marking: "1", on: { click: this.delForm, }, }, ], }, ], activeItem: { label: "动态", gutter: 10, children: [ { type: "input", prop: "active", span: 20 }, { type: "button", btnType: "primary", btnIcon: "el-icon-plus", span: 2, on: { click: this.addForm, }, }, { type: "button", btnIcon: "el-icon-delete-solid", span: 2, on: { click: this.delForm, }, }, ], }, columns4: [ { type: "text", label: "标题", prop: "title", default: "灵活表单" }, { type: "date", label: "日期", prop: "date", props: { valueFormat: "yyyy-MM-DD" }, }, { label: "自定义", prop: "input", render: ({ value, _this }) => { return ( <el-input value={value} onInput={(value) => { _this.setForm("input", value); }} /> ); }, }, ], }; }, methods: { handleSubmit(form, valid, callback) { console.log(form, valid); setTimeout(callback, 1000); }, addForm(e, item) { let marking = item.attrs.marking; let newMarking = Number(marking) + 1; let columns = cloneDeep(this.columns3); let index = this.columns3.findIndex((formItem) => formItem?.marking === marking) + 1; let addItem = cloneDeep(this.activeItem); addItem.label += newMarking; addItem.children[0].prop += newMarking; addItem.children[1].marking = newMarking; addItem.children[2].marking = newMarking; addItem.marking = newMarking; columns.splice(index, 0, addItem); this.columns3 = columns; }, delForm(e, item) { let marking = item.attrs.marking; if (marking === "1") return; let columns = cloneDeep(this.columns3); let index = this.columns3.findIndex( (formItem) => formItem?.marking === marking, ); columns.splice(index, 1); this.columns3 = columns; }, }, }; </script>
2、table
-
创建
components/modules/x-table/index.js
import XTable from "./index.vue"; export default XTable;
-
创建
components/modules/x-table/index.vue
<script> import { cloneDeep, isMatch } from "lodash"; import { delEmptyKey } from "@/utils/utils"; import dayjs from "dayjs"; /** * 配置说明文档 * @param { Boolean } autoHeight: 是否自动填充表格高度 * @param { Array } columns: 表格的配置项,见下面详细说明 * @param { Array } tableData: 表格的值 * @param { Object } search: 表单默认搜索值、或者api默认入参 * @param { Function } api: 接口 * @param { String } itemAlign: 单元格align风格(left、center、right) * @param { String } emptyText: 无数据描述 * @param { Any } emptyImage: 无数据图片展示地址 * @param { Number } emptyImageSize: 无数据图片大小 * @param { Boolean } hasPagination: 是否显示分页 * @param { String } paginationLayout: 分页显示内容 * @param { Boolean } multiple: 是否多选 * @param { Boolean } hasRadio: 是否单选 * * @event { function } toggleSelection:设置默认多选 * @event { function } setCurrent: 设置单选 * @event { function } clearFilter:清空筛选项 * @event { function } getSelection: 获取选中结果 * @event { function } getTableData: 获取表格值 */ /** * 表单配置项 columns-item * @param { Object } form: 单独作用form的配置 * @param { Object } table: 单独作用table的配置 * @param { Object } search: 单独作用search的配置 * 以上的配置和下面一样,但优先级高 * * * @param { String } type: 表单控件的类型。(参考element-ui的控件类型) * @param { String } label: 字段名称 * @param { String } prop: 字段绑定key * @param { Any } default: 控件默认值。(级别最高) * @param { String } placeholder: 提示信息 * @param { Array } rules: 校验规则。(级别最高) * @param { Number } order: 排序序号。(大到小) * @param { Number } span: 当前表单控件col占比。(级别高) * @param { Object } col: el-col的其他配置。(级别最高) * @param { String } labelWidth: label的宽度。(级别最高) * @param { Number } contentSpan: 内容占比宽度 * @param { Boolean } required: 是否必填 * @param { Boolean } disabled: 是否禁用。(权限最高) * * @param { Object } children: 子节点, (一个el-form-item里面多个控件) * @param { Number } gutter: 子节点的el-row的gutter。(仅在设置children后生效) * @param { Number, Array } grid: 子节点的栅格布局col排布 * * @param { Object } className: 表单控件的类名,对象的形式:{ box: true } * @param { Object } style: 样式控制 * @param { Object } attrs: element-ui表单控件的attribute,参考element-ui文档。 * @param { Object } props: element-ui表单控件的props。(有的时候文档上写的时attrs设置,其实时props设置。所以不生效的时候可以尝试一下) * @param { Object } on: 表单控件的绑定事件。{ input: (value) => console.log(value) } * @param { Array } options: radio,checkbox, select, cascader, * @param { String } text: button按钮的文字内容 * @param { String } btnType: button按钮的类型 * @param { String } btnIcon: button的icon * @param { String } marking: button的标记 * @param { Boolean } hasFilter: 是否进行筛选,只有存在options才可以进行 * * @param { Function } slotDefault: el-table-column 的 default的插槽 * @param { Function } slotHeader: el-table-column 的 header的插槽 */ export default { name: "XTable", props: { autoHeight: { type: Boolean, default: true, }, columns: { type: Array, default: () => [], }, tableData: { type: Array, default: () => [], }, search: { type: Object, default: () => ({}), }, api: { type: Function, default: null, }, itemAlign: { type: String, default: "left", }, emptyText: { type: String, default: "暂无数据", }, emptyImage: null, emptyImageSize: { type: Number, default: null, }, hasPagination: { type: Boolean, default: true, }, paginationLayout: { type: String, default: "total, sizes, prev, pager, next, jumper", }, multiple: { type: Boolean, default: false, }, hasRadio: { type: Boolean, default: false, }, }, data() { return { height: 0, fields: [], showData: [], searchForm: {}, pageInfo: { total: 0, pagesize: 10, current: 1, }, multipleSelection: [], loading: false, }; }, render(h) { return h( "div", { class: { "x-table-wrap": true, "x-table-radio": this.hasRadio }, style: { height: `${this.height}px` }, }, [this.renderTable(h), this.renderPagination(h)], ); }, watch: { columns: { immediate: true, deep: true, handler() { this.handleFields(); }, }, tableData: { deep: true, handler() { this.setTableData(this.searchForm); }, }, search: { immediate: true, deep: true, handler(val) { this.searchForm = val; }, }, }, mounted() { this.init(); }, methods: { init() { this.getParentHeight(); window.addEventListener("reset", this.getParentHeight.bind(this)); this.setTableData(this.searchForm); }, handleFields() { this.fields = this.handleFieldsChild(this.columns); if (this.multiple || this.hasRadio) { this.fields.unshift({ type: "selection", width: "40", }); } }, handleFieldsChild(columns) { let fields = columns.filter((item) => item?.isShow !== false); fields = fields.map((item) => { item.inputType = item.type; delete item.type; const tableItem = { ...item, ...item?.table }; delete tableItem.table; delete tableItem.search; delete tableItem.form; delete tableItem.dialog; if (tableItem.children && Array.isArray(tableItem.children)) { delete tableItem.prop; tableItem.children = this.handleFieldsChild(tableItem.children); } tableItem.align = tableItem.align || this.itemAlign; return tableItem; }); fields = fields.sort((a, b) => (b?.order || 0) - (a?.order || 0)); return fields; }, getParentHeight() { const parentDom = this.$el.parentElement; const height = parentDom.clientHeight; const paddingTop = parseFloat( getComputedStyle(parentDom, false)["padding-top"], ); const paddingBottom = parseFloat( getComputedStyle(parentDom, false)["padding-bottom"], ); this.height = height - paddingTop - paddingBottom; }, renderTable(h) { if (!this.height) return null; const config = { props: { highlightCurrentRow: this.hasRadio, ...this.$attrs, data: this.showData, }, on: { "selection-change": this.handleSelectionChange, "current-change": this.selectCurrentChange, }, ref: "table", directives: [{ name: "loading", value: this.loading }], }; if (this.autoHeight) { let paginationHeight = this.hasPagination ? 35 : 0; config.props.height = this.height - paginationHeight; } return h("el-table", config, [ ...this.renderColumns(h, this.fields), this.renderEmpty(h), this.$slots.default, ]); }, /* 空内容 */ renderEmpty(h) { if (this.showData && this.showData.length) return null; const config = { props: { description: this.emptyText }, slot: "empty", }; if (this.emptyImage) config.props.image = this.emptyImage; if (this.emptyImageSize) config.props.imageSize = this.emptyImageSize; return h("el-empty", config); }, /* table-column */ renderColumns(h, fields) { return fields.map((item) => this.renderTableItem(h, item)); }, renderTableItem(h, item) { const scopedSlots = {}; if (item.slotDefault && typeof item.slotDefault === "function") { scopedSlots.default = (scope) => item.slotDefault(this.getHypeScript(), scope); } if (item.slotHeader && typeof item.slotHeader === "function") { scopedSlots.header = (scope) => item.slotHeader(this.getHypeScript(), scope); } if (!item.slotDefault && item.children && Array.isArray(item.children)) { item.childrenDom = this.renderColumns(h, item.children); } const attrs = { ...item, ...item?.attrs }; let props = item.props; delete item.props; delete item.attrs; // 后续需要添加字典 if (!item.children && item.options) { props = { formatter: (row, column) => { const property = column["property"]; const value = row[property]; const findItem = item.options.find((item) => item.value === value); return findItem.label; }, ...props, }; } let formatType = ["time-select", "time", "date"]; if ( !item.slotDefault && item?.props?.valueFormat && formatType.includes(item.inputType) ) { let formatReg = item.props.valueFormat.replace(/yyyy/g, "YYYY"); props = { formatter: (row, column) => { const property = column["property"]; const value = row[property]; return dayjs(value).format(formatReg); }, ...props, }; } if (item.options && item.hasFilter) { props = { ...props, filters: item.options.map(({ label, value }) => ({ text: label, value, })), filterMethod: (value, row, column) => { const property = column["property"]; return row[property] === value; }, }; } return h( "el-table-column", { attrs, props, scopedSlots }, item.childrenDom, ); }, /* 分页 */ renderPagination(h) { if (!this.hasPagination) return null; return h("div", { class: { "x-table-bottom": true } }, [ h("el-pagination", { props: { layout: this.paginationLayout, ...this.pageInfo, }, on: { "size-change": this.handleSizeChange, "current-change": this.handleCurrentChange, }, }), this.multiple && h("div", null, [`已选中 ${this.multipleSelection.length} 个`]), ]); }, handleSizeChange(size) { this.pageInfo.pagesize = size; this.setTableData(this.searchForm); }, handleCurrentChange(current) { this.pageInfo.current = current; this.setTableData(this.searchForm); }, /* 方法 */ getHypeScript() { return this.$parent.$createElement; }, setTableData(search) { this.searchForm = search; let searchForm = delEmptyKey(search); let handleResolve = null; const promiseObj = new Promise((resolve) => { handleResolve = resolve; }); if (this.tableData && this.tableData.length) { this.loading = true; const { pagesize, current } = this.pageInfo; let tableData = cloneDeep(this.tableData); tableData = tableData.filter((item) => isMatch(item, searchForm)); this.pageInfo.total = tableData.length; this.showData = [...tableData].splice( (current - 1) * pagesize, pagesize, ); this.loading = false; handleResolve(); handleResolve = null; } else if (this.api && typeof this.api === "function") { const param = { ...searchForm, pagesize: this.pageInfo.pagesize, pageno: this.pageInfo.current, }; this.loading = true; // 需要和后端协商好接口字段,保持所有table接口返回统一 this.api(param) .then((res) => { this.showData = res.data.records; const { total, current, pagesize } = res.data; this.pageInfo = { total, current, pagesize }; }) .catch(() => { this.showData = []; this.pageInfo = { total: 0, current: 1, pagesize: 10, }; }) .finally(() => { this.loading = false; handleResolve(); handleResolve = null; }); } return promiseObj; }, handleSelectionChange(val) { if (this.hasRadio && val.length > 1) { const delItem = this.multipleSelection[0]; const item = val.find((child) => !isMatch(child, delItem)); this.$refs.table.clearSelection(); this.setCurrent(item); } else { this.multipleSelection = val; } }, toggleSelection(rows = []) { if (!this.multiple) return false; if (rows) { rows.forEach((row) => { const findItem = this.findRow(row); this.$refs.table.toggleRowSelection(findItem); }); } else { this.$refs.table.clearSelection(); } }, selectCurrentChange(val) { this.multipleSelection = [val]; if (this.hasRadio) { this.$refs.table.clearSelection(); const findItem = this.findRow(val); this.$refs.table.toggleRowSelection(findItem); } }, setCurrent(row) { if (!this.hasRadio) return false; this.$refs.table.setCurrentRow(row); }, findRow(row) { return this.showData.find((item) => isMatch(item, row)); }, clearFilter(key) { this.$refs.table.clearFilter(key); }, getSelection() { return this.multipleSelection; }, getTableData() { return this.showData; }, }, }; </script> <style lang="less" scoped> .x-table-wrap { width: 100%; .x-table-bottom { display: flex; justify-content: space-between; align-items: center; margin-top: 3px; font-size: 13px; font-weight: 400; color: #606266; flex-direction: row-reverse; } } .x-table-radio { ::v-deep { .el-table__header-wrapper { .el-checkbox { display: none; } } } } </style>
-
使用案例
<template> <div class="wrap"> <!-- 基础空表单 --> <h3>基础空表单</h3> <div class="box"> <x-table itemAlign="center" :border="true" :autoHeight="false" :hasPagination="false" :columns="columns1" :tableData="[]" ></x-table> </div> <!-- 基础表单 --> <h3>基础表单</h3> <div class="box"> <x-table itemAlign="center" :border="true" :autoHeight="false" :hasPagination="false" :columns="columns1" :tableData="tableData1" ></x-table> </div> <!-- 自动填充和分页 --> <h3>自动填充和分页</h3> <p>自动填充高度,并且能自动处理radio、checkbox、select的值</p> <div class="box"> <x-table itemAlign="center" :border="true" :columns="columns2" :tableData="tableData2" ></x-table> </div> <!-- 自定义内容 --> <h3>自定义内容</h3> <div class="box"> <x-table itemAlign="center" :border="true" :columns="columns3" :tableData="tableData3" ></x-table> </div> <!-- 多表头 --> <h3>多表头</h3> <div class="box"> <x-table itemAlign="center" :border="true" :columns="columns4" :tableData="tableData4" ></x-table> </div> <!-- 多选 --> <h3>多选</h3> <div> <el-button @click="getSelection('multipleTable')">获取选中结果</el-button> <el-button @click="setSelection([tableData1[1], tableData1[2]])"> 设置选择第二项, 第三项 </el-button> <el-button @click="setSelection()">清空选中</el-button> </div> <div class="box"> <x-table ref="multipleTable" itemAlign="center" :border="true" :multiple="true" :columns="columns1" :tableData="tableData1" ></x-table> </div> <!-- 单选 --> <h3>单选</h3> <div> <el-button @click="getSelection('singleTable')">获取选中结果</el-button> <el-button @click="setRadio(tableData1[2])"> 设置选择第三项 </el-button> <el-button @click="setRadio()">清空选中</el-button> </div> <div class="box"> <x-table ref="singleTable" itemAlign="center" :border="true" :hasRadio="true" :columns="columns1" :tableData="tableData1" ></x-table> </div> <!-- 排序和筛选 --> <h3>排序和筛选</h3> <div class="box"> <x-table itemAlign="center" :border="true" :autoHeight="false" :hasPagination="false" :columns="columns5" :tableData="tableData5" ></x-table> </div> <!-- 嵌套内容 --> <h3>嵌套内容</h3> <div class="box"> <x-table itemAlign="center" :border="true" :autoHeight="false" :hasPagination="false" :columns="columns6" :tableData="tableData5" ></x-table> </div> <!-- 接口获取 --> <h3>接口获取</h3> <div class="box"> <x-table itemAlign="center" :border="true" :columns="columns7" :api="getList" ></x-table> </div> <!-- 搜索 --> <h3>搜索</h3> <div class="box"> <x-table itemAlign="center" :border="true" :columns="columns7" :api="getList" :search="{ name: '张三' }" ></x-table> </div> </div> </template> <script> import { getList } from "@/api/mock.js"; export default { name: "Page5", data() { return { search: "", columns1: [ { label: "姓名", prop: "name" }, { label: "年龄", prop: "age" }, { label: "地址", prop: "address" }, ], tableData1: [ { name: "张三1", age: 12, address: "北京市" }, { name: "张三2", age: 12, address: "北京市" }, { name: "张三3", age: 12, address: "北京市" }, { name: "张三4", age: 12, address: "北京市" }, { name: "张三5", age: 12, address: "北京市" }, ], columns2: [ { label: "姓名", prop: "name" }, { label: "年龄", prop: "age" }, { label: "性别", prop: "sex", options: [ { label: "男", value: "0" }, { label: "女", value: "1" }, ], }, { label: "地址", prop: "address" }, ], tableData2: [ { name: "张三", age: 12, sex: "0", address: "北京市" }, { name: "张三", age: 12, sex: "1", address: "北京市" }, { name: "张三", age: 12, sex: "0", address: "北京市" }, { name: "张三", age: 12, sex: "1", address: "北京市" }, { name: "张三", age: 12, sex: "0", address: "北京市" }, { name: "张三", age: 12, sex: "1", address: "北京市" }, { name: "张三", age: 12, sex: "0", address: "北京市" }, { name: "张三", age: 12, sex: "1", address: "北京市" }, { name: "张三", age: 12, sex: "0", address: "北京市" }, { name: "张三", age: 12, sex: "1", address: "北京市" }, { name: "张三", age: 12, sex: "0", address: "北京市" }, { name: "张三", age: 12, sex: "1", address: "北京市" }, { name: "张三", age: 12, sex: "0", address: "北京市" }, { name: "张三", age: 12, sex: "1", address: "北京市" }, { name: "张三", age: 12, sex: "0", address: "北京市" }, { name: "张三", age: 12, sex: "1", address: "北京市" }, { name: "张三", age: 12, sex: "0", address: "北京市" }, { name: "张三", age: 12, sex: "1", address: "北京市" }, { name: "张三", age: 12, sex: "0", address: "北京市" }, { name: "张三", age: 12, sex: "1", address: "北京市" }, { name: "张三", age: 12, sex: "0", address: "北京市" }, { name: "张三", age: 12, sex: "1", address: "北京市" }, { name: "张三", age: 12, sex: "0", address: "北京市" }, ], columns3: [ { label: "姓名", prop: "name" }, { label: "年龄", prop: "age" }, { label: "地址", prop: "address" }, { slotHeader: () => { return ( <el-input value={this.search} onInput={(val) => (this.search = val)} size="mini" placeholder="输入关键字搜索" /> ); }, slotDefault: (h, scope) => { return ( <div> <el-button size="mini" onClick={() => this.handleEdit(scope.$index, scope.row)} > Edit </el-button> <el-button size="mini" type="danger" onClick={() => this.handleDelete(scope.$index, scope.row)} > Delete </el-button> </div> ); }, }, ], tableData3: [ { name: "张三", age: 12, address: "北京市" }, { name: "张三", age: 12, address: "北京市" }, { name: "张三", age: 12, address: "北京市" }, ], columns4: [ { label: "用户", children: [ { label: "姓名", prop: "name" }, { label: "年龄", prop: "age" }, { label: "信息", children: [ { label: "身高", prop: "height" }, { label: "体重", prop: "weight" }, { label: "性别", prop: "sex", options: [ { label: "男", value: "0" }, { label: "女", value: "1" }, ], }, ], }, ], }, { label: "地址", prop: "address" }, ], tableData4: [ { name: "张三", age: 12, sex: "0", height: "175cm", weight: "65kg", address: "北京市", }, { name: "张三", age: 12, sex: "1", height: "175cm", weight: "65kg", address: "北京市", }, { name: "张三", age: 12, sex: "0", height: "175cm", weight: "65kg", address: "北京市", }, ], columns5: [ { label: "姓名", prop: "name" }, { label: "年龄", prop: "age", sortable: true }, { label: "性别", prop: "sex", hasFilter: true, options: [ { label: "男", value: "0" }, { label: "女", value: "1" }, ], }, { label: "地址", prop: "address" }, ], tableData5: [ { name: "张三1", age: 12, sex: "1", address: "北京市" }, { name: "张三2", age: 13, sex: "0", address: "北京市" }, { name: "张三3", age: 14, sex: "0", address: "北京市" }, { name: "张三4", age: 15, sex: "1", address: "北京市" }, { name: "张三5", age: 16, sex: "0", address: "北京市" }, ], columns6: [ { table: { type: "expand" }, slotDefault: (h, scope) => { return ( <el-form label-position="left"> <el-form-item label="姓名"> <span>{scope.row.name}</span> </el-form-item> <el-form-item label="年龄"> <span>{scope.row.age}</span> </el-form-item> <el-form-item label="性别"> <span>{scope.row.sex}</span> </el-form-item> <el-form-item label="地址"> <span>{scope.row.address}</span> </el-form-item> </el-form> ); }, }, { label: "姓名", prop: "name" }, { label: "年龄", prop: "age", sortable: true }, { label: "性别", prop: "sex", hasFilter: true, options: [ { label: "男", value: "0" }, { label: "女", value: "1" }, ], }, { label: "地址", prop: "address" }, ], columns7: [ { label: "姓名", prop: "name" }, { label: "年龄", prop: "age" }, { label: "性别", prop: "sex", options: [ { label: "男", value: "0" }, { label: "女", value: "1" }, ], }, { label: "日期", prop: "date", props: { valueFormat: "yyyy-MM-DD" } }, { label: "地址", prop: "address" }, ], }; }, methods: { getList: getList, handleEdit(index, row) { console.log(index, row); }, handleDelete(index, row) { console.log(index, row); }, getSelection(key) { const selection = this.$refs[key].getSelection(); console.log(selection); }, setSelection(rows) { this.$refs.multipleTable.toggleSelection(rows); }, setRadio(row) { this.$refs.singleTable.setCurrent(row); }, }, }; </script> <style lang="less" scoped> .wrap { width: 100%; height: 100%; display: flex; align-items: center; flex-direction: column; } .box { width: 800px; height: 500px; padding: 10px; margin-bottom: 20px; background: #1f03034d; } </style>
-
mockjs
function getList() { let list = new Array(99).fill({}); list = list.map((item, index) => { return { name: index > 20 ? `张三${index}` : "张三", age: index.toString(), date: Mock.mock("@date('yyyy-MM-dd')"), sex: (index % 2).toString(), address: `北京市朝阳区${index}号`, }; }); return new MockPort({ template: { code: 200, msg: "success", data: { records: [], pagesiez: 0, current: 1, total: 0, }, }, action(options) { const params = this.paramsBackRes(options.body) || {}; let { pagesize, pageno, ...search } = params; pagesize = pagesize || 10; pageno = pageno || 1; let records = list.filter((item) => isMatch(item, search)); this.template.data = { records: [...records].splice((pageno - 1) * pagesize, pagesize), total: records.length, pagesize, current: pageno, }; return this.template; }, }); }
3、dialog
-
创建
components/dialog/index.js
import XDialog from "./index.vue"; export default XDialog;
-
创建
components/dialog/index.vue
<template> <el-dialog :title="title" v-bind="$attrs" v-on="$listeners" :visible.sync="dialogVisible" @close="close" > <template slot="title"> <slot name="title"></slot> </template> <slot></slot> <template slot="footer"> <slot v-if="!hasHandle" name="footer" :submit="handleSubmit" :cancel="handleCancel" ></slot> <div v-else> <el-button v-if="cancel" @click="handleCancel"> {{ cancelText }} </el-button> <el-button v-if="submit" type="primary" @click="handleSubmit" :loading="btnloading" :disabled="sumbitDiasbled" :icon="submitIcon" > {{ submitText }} </el-button> </div> </template> </el-dialog> </template> <script> import { debounce } from "lodash"; import { Transmit } from "@/utils/utils"; /** * @param { String } title: 标题 * @param { Boolean } hasHandle: 控件按钮 * @param { Boolean } submit: 确认按钮是否显示 * @param { String } submitText: 确认按钮文字 * @param { String } submitIcon: 确认按钮icon * @param { Boolean } submitLoading: 确认按钮是否开启loading * @param { Boolean } sumbitDiasbled: 确认按钮是否禁用 * @param { Boolean } cancel: 取消按钮是否显示 * @param { Boolean } cancelText: 取消按钮文字 * * @event { Function } open: 打开dialog,返回promise * @event { Function } handleSubmit: 手动触发确认 * @event { Function } handleCancel: 手动出发取消并关闭dialog */ export default { name: "XDialog", props: { title: { type: String, default: "dialog", }, hasHandle: { type: Boolean, default: true, }, submit: { type: Boolean, default: true, }, submitText: { type: String, default: "确认", }, submitIcon: { type: String, default: "", }, submitLoading: { type: Boolean, default: false, }, sumbitDiasbled: { type: Boolean, default: false, }, cancel: { type: Boolean, default: true, }, cancelText: { type: String, default: "取消", }, }, data() { return { dialogVisible: false, btnloading: false, resolveHandle: null, rejectHandle: null, submitHandle: null, }; }, methods: { open() { return new Transmit((resolve, reject) => { this.resolveHandle = resolve; this.rejectHandle = reject; this.dialogVisible = true; }); }, handleSubmit: debounce(() => { if (this.submitLoading) this.btnloading = true; const callback = (closeStatus = true) => { this.btnloading = false; closeStatus && this.close(); }; this.resolveHandle(callback.bind(this)); }, 500), handleCancel: debounce(() => { this.rejectHandle(); this.close(); }, 500), close() { this.dialogVisible = false; this.btnloading = false; this.resolveHandle = null; this.rejectHandle = null; }, }, }; </script>
-
使用案例
<template> <div> <el-button type="text" @click="openDialog1"> 点击打开 Dialog1 </el-button> <x-dialog ref="dialog1" :submitLoading="true"> <span slot="title">我的标题</span> <span>这是一段信息</span> </x-dialog> <el-button @click="openDialog2"> 点击打开 Dialog2 </el-button> <x-dialog ref="dialog2" :submitLoading="true" :hasHandle="false"> <span>这是一段信息</span> <template v-slot:footer="scoped"> <el-button @click="() => handleCancel(scoped.cancel)">取消</el-button> <el-button @click="handleNext">下一步</el-button> <el-button @click="() => handleSubmit(scoped.submit)">提交</el-button> </template> </x-dialog> </div> </template> <script> export default { name: "Page6", data() { return { dialogVisible: false, }; }, mounted() {}, methods: { openDialog1() { this.$refs.dialog1 .open() .then((callback) => { setTimeout(callback, 1000); }) .catch(() => {}); }, openDialog2() { this.$refs.dialog2 .open() .then((callback) => { setTimeout(callback, 1000); }) .catch(() => {}); }, handleCancel(cancel) { this.$confirm("是否取消并关闭弹窗?", "提示", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning", }).then(() => { cancel(); this.$message({ type: "success", message: "取消成功!", }); }); }, handleNext() { this.$message({ type: "success", message: "下一步成功!", }); }, handleSubmit(submit) { this.$confirm("是否提交并关闭弹窗?", "提示", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning", }).then(() => { submit(); this.$message({ type: "success", message: "提交成功!", }); }); }, }, }; </script>