基于vue3和element-plus封装的curd组件,支持el-table和el-tree,类似avue-curd
本文连接:https://www.cnblogs.com/muphy/p/15826954.html
写这个的主要目的还是为了学习vue3,为了快速学习,我从gitee找到了一个非常优秀的基于VUE3和element-plus的前端admin框架,地址:https://gitee.com/asaasa/vue3-element-admin,并进行了部分优化
对于前端快速发展,vue3.x的出现让很多基于vue开发的UI框架失去了兼容性,特别是avue-curd这样优秀的组件无法使用,而最新的技术肯定相对于老技术有一定的优势,我本想查找一些现有的基于vue3.x的curd组件来直接使用,找了半天也不尽人意,于是自己写一个类似avue-curd的组件,基本上兼容curd组件使用的element-plus的组件的所有属性,如果哪里有疑问查询element-plus对应组件api就行,这里主要以azi模块介绍使用方法,组件源码在最后面。
支持el-table和el-tree,绑定参数: crudType="tree"、默认值:“table”
有帮助的话给个关注,有问题的画帮忙指出问题
安装
npm i ve-curd
import VeCurd from "ve-curd";
app.use(VeCurd);
源码下载
git clone https://gitee.com/muphy1112/ve-curd.git
先看效果
分别点击查看,新增、编辑和删除:
表格默认支持过滤,排序和分页
el-table和el-pagination所有特性基本上都可以使用,红色圈内是新增的,其他还有什么尽管绑定即可,另外冲突的disabled需要分开
表单组件根据表格列对象配置的type值区分组合了:el-input、el-cascader、el-checkbox、el-data-picker,这些组件的属性都通过表格列对象配置,也支持字典
增删改查表单的label和value都可以被自定义,对应加上功能名称,如列对象prop配置为username:那么自定义包括username、usernameValue、usernameLabel、usernameForm、usernameView、usernameEdit、usernameAdd等
表单本身也支持自定义:editForm、viewForm、addForm等
还有一些就不列举了,哪里有问题直接改组件源码
测试页面:examples/components/HelloWorld.vue
<!-- * @Author: ruphy若非 * @Date: 2022-01-20 11:03:21 * @Description: curd --> <template> <div style="width: 100%; height: 800px"> <el-row :gutter="20"> <el-col :span="4"> <ve-curd curd-type="tree" :data="treeData" :columns="tableColumns" :props="{ label: 'realName', children: 'children', }" @delete-sure="handleDelete" @edit-sure="handleEdit" @add-sure="handleAdd" ></ve-curd> </el-col> <el-col :span="20"> <!-- 列表 --> <ve-curd :model="form" show-message :data="tableData" :columns="tableColumns" :total="page.total" v-model:page-size="page.pageSize" v-model:current-page="page.currentPage" v-model:ascs="page.ascs" v-model:descs="page.descs" v-model:filters="page.filters" @sort-change="handleChange" @size-change="handleChange" @current-change="handleChange" @filter-change="searchChange" @search-change="searchChange" @delete-sure="handleDelete" @edit-sure="handleEdit" @add-sure="handleAdd" > </ve-curd> </el-col> </el-row> </div> </template> <script> export default { name: "HelloWorld", data: () => ({ description: "用户信息查询与设置", menus: { search: { name: "查询" }, add: { name: "添加" }, edit: { name: "编辑" }, del: { name: "删除" }, }, }), }; </script> <script setup> import * as azi from "../api/user"; import { getCurrentInstance, reactive, ref, unref, onMounted } from "vue"; const { proxy } = getCurrentInstance(); //获取上下文实例,ctx=vue2的this const form = reactive({ name: "", }); const page = reactive({ total: 0, // 总页数 currentPage: 1, // 当前页数 pageSize: 10, // 每页显示多少条 ascs: [], //升序字段 descs: [], //降序字段 filters: [], //降序字段 }); const tableData = ref([]); const tableColumns = ref([ { type: "selection", }, { label: "ID", prop: "id", search: true, sortable: "custom", addable: true, editable: true, viewable: true, filters: [ { text: "第一个", value: "1" }, { text: "第二个", value: "2" }, { text: "第三个", value: "3" }, { text: "第四个", value: "4" }, ], filterMethod: (value, row, column) => { const property = column["property"]; return row[property] === value; }, rules: [ { required: true, message: "请输入ID", trigger: "blur", }, { max: 32, message: "长度在不能超过32个字符", }, ], }, { label: "用户账号", prop: "username", hide: true, search: true, sortable: true, addable: true, editable: true, viewable: true, type: "url", rules: [ { max: 32, message: "长度在不能超过32个字符", }, ], }, { label: "用户名称", prop: "realName", sortable: true, search: true, addable: true, editable: true, viewable: true, maxlength: 55, type: "textarea", rules: [ { max: 32, message: "长度在不能超过32个字符", }, ], }, { label: "用户密码", prop: "password", type: "password", hide: true, search: true, sortable: true, addable: true, editable: true, viewable: true, rules: [ { max: 32, message: "长度在不能超过32个字符", }, ], resetField: (e) => { alert("dd" + e); }, }, { label: "角色ID", prop: "roleIds", search: true, sortable: true, addable: true, editable: true, viewable: true, type: "number", rules: [ { max: 32, message: "长度在不能超过32个字符", }, ], }, { label: "状态:0-禁用,1-启用", prop: "status", search: true, sortable: true, addable: true, editable: true, viewable: true, type: "select", dict: { options: [ { text: "禁用", label: "禁用", value: "0" }, { text: "启用", label: "启用", value: "1" }, ], }, }, ]); const handleDelete = (row) => { console.log("handleDelete", row); azi.delObj(row).then((res) => { if (res.success) { searchChange(); } else { throw new Error(res.msg); } }); }; const handleEdit = (row) => { console.log("handleEdit", row); azi.putObj(row).then((res) => { if (res.success) { searchChange(); } else { throw new Error(res.msg); } }); }; const handleAdd = (row) => { console.log("handleAdd", row); azi.addObj(row).then((res) => { if (res.success) { searchChange(); } else { throw new Error(res.msg); } }); }; const searchChange = () => { let p = unref(page); p.total = 0; return handleChange(); }; const handleChange = () => { let p = unref(page); let params = Object.assign({}, p, form); console.log("handleChange", params); return azi.getPage(params).then((d) => { if (d.data.success) { p.total = d.data.data.total; tableData.value = d.data.data.records || []; } }); }; handleChange(); // tree const treeData = ref([]); const loadTreeData = () => { return azi.getList().then((d) => { if (d.data.success) { treeData.value = d.data.data || []; } }); }; loadTreeData(); onMounted(() => { console.log(proxy, proxy); }) </script> <style lang="scss" scoped></style>
examples/api/user.js
export function getPage(query) { return Promise.resolve({ data: { success: true, data: { total: 58, records: [{"admin":true,"id":"1","lastTime":1642969720288,"password":"123456","realName":"超级管理员","roleIds":"1","status":"1","username":"admin"},{"admin":false,"id":"11","lastTime":1642969720288,"password":"440","realName":"jjj","roleIds":"2","status":"0","username":"qqq"},{"admin":false,"id":"13","lastTime":1642969720288,"password":"111111","realName":"宝宝","roleIds":"2","status":"0","username":"aaa"},{"admin":false,"id":"2","lastTime":1642969720288,"password":"123456","realName":"管理员","roleIds":"2","status":"1","username":"test"},{"admin":false,"id":"3","lastTime":1642969720288,"realName":"安安","roleIds":"","status":"0"},{"admin":false,"id":"4","lastTime":1642969720288,"realName":"静静","roleIds":"","status":"0","username":"静静"},{"admin":false,"id":"5","lastTime":1642969720288,"realName":"阿朱","roleIds":"","status":"0"},{"admin":false,"id":"6","lastTime":1642969720288,"realName":"武怡","roleIds":"","status":"0"},{"admin":false,"id":"7","lastTime":1642969720288,"password":"123","realName":"阿紫","roleIds":"2","status":"0","username":"azi"},{"admin":false,"id":"8","lastTime":1642969720288,"realName":"爱玲","roleIds":""}]}}}); // return request({ // url: '/gw/user/page', // method: 'get', // params: query // }) } export function getList(query) { return Promise.resolve({ data: { success: true, data: [{"admin":true,"id":"1","lastTime":1642969720288,"password":"123456","realName":"超级管理员","roleIds":"1","status":"1","username":"admin"},{"admin":false,"id":"11","lastTime":1642969720288,"password":"440","realName":"jjj","roleIds":"2","status":"0","username":"qqq"},{"admin":false,"id":"13","lastTime":1642969720288,"password":"111111","realName":"宝宝","roleIds":"2","status":"0","username":"aaa"},{"admin":false,"id":"2","lastTime":1642969720288,"password":"123456","realName":"管理员","roleIds":"2","status":"1","username":"test"},{"admin":false,"id":"3","lastTime":1642969720288,"realName":"安安","roleIds":"","status":"0"},{"admin":false,"id":"4","lastTime":1642969720288,"realName":"静静","roleIds":"","status":"0","username":"静静"},{"admin":false,"id":"5","lastTime":1642969720288,"realName":"阿朱","roleIds":"","status":"0"},{"admin":false,"id":"6","lastTime":1642969720288,"realName":"武怡","roleIds":"","status":"0"},{"admin":false,"id":"7","lastTime":1642969720288,"password":"123","realName":"阿紫","roleIds":"2","status":"0","username":"azi"},{"admin":false,"id":"8","lastTime":1642969720288,"realName":"爱玲","roleIds":""},{"admin":false,"id":"9","lastTime":1642969720288,"realName":"剑圣","roleIds":""}]}}); // return request({ // url: '/gw/user/list', // method: 'get', // params: query // }) }
CURD组件可以使用全局注册或者单独import都可以,文件路径和内容:packages/ve-curd/src/VeCurd.vue

<!-- * @Author: ruphy若非 * @Date: 2022-01-20 11:03:21 * @Description: curd --> <template> <div style="height: 100%; width: 100%; min-height: 500px"> <div style="height: 100%; width: 100%" v-if="curdType === 'tree'"> <el-input v-model="treeQuery" clearable :suffix-icon="Search" placeholder="请输入..." @input="onTreeQueryChanged" > <template #append> <el-button :icon="Plus" @click="handleTreeAdd(null, {})" ></el-button> </template> </el-input> <el-tree ref="treeRef" :indent="10" empty-text="当前没有数据" v-bind="$attrs" :data="data" :props="props" :filter-method="filterHandle" > <template #default="scope"> <slot :node="scope.node" :data="scope.data"> <div style="width: 100%; height: 100%"> <el-dropdown trigger="contextmenu"> <el-icon> <Folder v-if="!scope.node.isLeaf" /> <Document v-else /> <el-tooltip :content="scope.node.label" placement="bottom" :show-after="1000" effect="light" > <span>{{ scope.node.label }}</span> </el-tooltip> </el-icon> <template #dropdown> <el-dropdown-menu> <el-dropdown-item :icon="Plus" @click=" handleTreeAdd( scope.node, scope.data ) " > {{ (permission && permission.add && permission.add.name) || "新增" }} </el-dropdown-item> <el-dropdown-item :icon="Delete" @click=" handleTreeDelete( scope.node, scope.data ) " > {{ (permission && permission.del && permission.del.name) || "删除" }} </el-dropdown-item> <el-dropdown-item :icon="Edit" @click=" handleTreeEdit( scope.node, scope.data ) " > {{ (permission && permission.edit && permission.edit.name) || "编辑" }} </el-dropdown-item> <el-dropdown-item :icon="View" @click=" handleTreeView( scope.node, scope.data ) " > 查看 </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> </slot> </template> </el-tree> </div> <div style="width: 100%; height: 100%" v-else> <el-card v-show="searchShow && searchColumns.length > 0" style="margin-bottom: 20px" > <el-form ref="queryFormRef" :model="$attrs.model" :inline="inline" :label-position="labelPosition" :label-width="labelWidth" :label-suffix="labelSuffix" :hide-required-asterisk="hideRequiredAsterisk" :show-message="showMessage" :inline-message="inlineMessage" :status-icon="statusIcon" :validate-on-rule-change="validateOnRuleChange" :size="size" :disabled="formDisabled" :validate="validate" :validateField="validateField" :resetFields="resetFields" :scrollToField="scrollToField" :clearValidate="clearValidate" @validate="formValidate" > <template #default> <slot name="searchForm"> <template v-for="(item, index) in searchColumns" :key="index" > <el-form-item :prop="item.prop" :label="item.label" :label-width="item.labelWidth" :required="item.required" :error="item.error" :show-message="item.showMessage" :inline-message="item.inlineMessage" :size="item.size" :resetField="item.resetField" :clearValidate="item.clearValidate" > <template #default> <!-- 自定义列的内容slot改名为以属性名称命名的slot --> <slot :name="item.prop + 'Search'"> <el-date-picker v-if="item.type === 'daterange'" clearable v-model=" $attrs.model[item.prop] " type="daterange" range-separator="~" start-placeholder="开始日期" end-placeholder="结束日期" :value-format=" item.valueFormat || 'YYYY-MM-DD' " > </el-date-picker> <el-date-picker v-else-if=" item.type === 'datetimerange' " clearable v-model=" $attrs.model[item.prop] " type="datetimerange" range-separator="~" start-placeholder="开始时间" end-placeholder="结束时间" :value-format=" item.valueFormat || 'YYYY-MM-DD hh:mm:ss' " > </el-date-picker> <el-checkbox v-else-if=" item.type === 'checkbox' " clearable v-model=" $attrs.model[item.prop] " :label="$attrs.model[item.prop]" style="width: 220px" ></el-checkbox> <el-cascader v-else-if=" item.type === 'select' || item.type === 'multiselect' || item.type === 'cascader' " clearable filterable v-bind="item.dict" style="width: 220px" v-model=" $attrs.model[item.prop] " ></el-cascader> <el-input v-else clearable autosize placeholder="请输入..." style="width: 220px" v-bind="item" :type="item.type || 'text'" :show-password=" item.type === 'password' " v-model=" $attrs.model[item.prop] " ></el-input> </slot> </template> <template #label="scope"> <!-- 自定义label的内容slot改名为以属性名开头的slot --> <slot :name="item.prop + 'Label'" :key="scope.key" :label="scope.label" > {{ scope.label }} </slot> </template> <template #error="scope"> <!-- 自定义错误slot改名为以属性名开头的slot --> <slot :name="item.prop + 'Error'" :error="scope.error" > <div class="el-form-item__error"> {{ scope.error }} </div> </slot> </template> </el-form-item> </template> <div style="text-align: right"> <el-form-item> <slot name="searchButton"> <el-button type="primary" @click="handleSearch" > 查询 </el-button> </slot> <slot name="resetButton"> <el-button @click="resetForm" style="margin-left: 30px" > 重置 </el-button> </slot> </el-form-item> </div> </slot> </template> </el-form> </el-card> <el-card> <!-- 工具类 --> <div style="margin-top: 20px"> <el-row> <el-col :span="20"> <slot name="addButton"> <el-button type="primary" :icon="Plus" @click="handleAdd" > {{ (permission && permission.add && permission.add.name) || "新增" }} </el-button> </slot> <slot name="toolbar"></slot> </el-col> <el-col :span="4" style="text-align: center"> <el-button type="text" :icon="Search" @click="handleSearchShow" ></el-button> <el-button type="text" :icon="Expand" @click="initDrawerColumnTree" ></el-button> </el-col> </el-row> </div> <!-- 表格 --> <el-table style="width: 100%" v-bind="$attrs" :data="data" @sort-change="handleSortChange" @filter-change="handleFilterChange" > <template #append> <slot name="append"></slot> </template> <!-- 表格列 --> <template v-for="(item, index) in tableColumns" :key="index" > <el-table-column v-bind="item" v-if="item.hide !== true" :column-key="item.prop" > <template #default="scope" v-if=" item.type !== 'selection' && item.type !== 'index' && item.type !== 'expand' " > <!-- 自定义列的内容slot改名为以属性名称命名的slot --> <slot :name="item.prop + 'Value'" :row="scope.row" :column="item" :$index="index" > {{ getLabel(scope.row[item.prop], item) }} </slot> </template> <template #header="scope"> <!-- 自定义表头的内容slot改名为以属性名称开头的slot --> <slot :name="item.prop + 'Header'" :column="item" :$index="index" > {{ scope.column.label }} </slot> </template> </el-table-column> </template> <!-- 操作 --> <el-table-column fixed="right" label="操作" width="180"> <template #default="scope"> <slot name="viewButton" :row="scope.row" :$index="scope.$index" > <el-button size="small" type="text" :icon="View" @click="handleView(scope.row, scope.$index)" > {{ (permission && permission.search && permission.search.name) || "查看" }} </el-button> </slot> <slot name="editButton" :row="scope.row" :$index="scope.$index" > <el-button size="small" type="text" :icon="Edit" @click="handleEdit(scope.row, scope.$index)" > {{ (permission && permission.edit && permission.edit.name) || "编辑" }} </el-button> </slot> <slot name="delButton" :row="scope.row" :$index="scope.$index" > <el-button size="small" type="text" style="color: red" :icon="Delete" @click=" handleDelete(scope.row, scope.$index) " > {{ (permission && permission.del && permission.del.name) || "删除" }} </el-button> </slot> <slot name="operate" :$index="scope.$index" :row="scope.row" ></slot> </template> </el-table-column> </el-table> <!-- 分页 --> <el-pagination :small="small" :background="background" :total="total" :page-count="pageCount" :pager-count="pagerCount" :page-sizes="pageSizes" :popper-class="popperClass" :prev-text="prevText" :next-text="nextText" :disabled="paginationDisabled" :hide-on-single-page="hideOnSinglePage" :default-current-page="defaultCurrentPage" :layout="layout" @size-change="handleSizeChange" @current-change="handleCurrentChange" > </el-pagination> </el-card> <el-drawer v-model="drawerColumnShow" direction="rtl" title="自定义列显示或隐藏" > <el-tree ref="drawerColumnTree" :data="tableColumns" :props="{ label: 'label' }" node-key="prop" show-checkbox @check-change="drawerTreeChange" /> </el-drawer> </div> <!-- 查看 --> <el-dialog v-model="dialogViewFormVisible" :title="viewTitle" @open="handleViewOpen" @opened="handleViewOpened" @close="handleViewClose" @closed="handleViewClosed" > <template #title> <el-icon><View /></el-icon> <span>{{ viewTitle }}</span> </template> <el-form ref="viewFormRef" :model="form" :inline="inline" :label-position="labelPosition" :label-width="labelWidth" :label-suffix="labelSuffix" :status-icon="statusIcon" :size="size" :disabled="true" > <template #default> <slot name="editForm" :form="form"> <template v-for="(item, index) in viewColumns" :key="index" > <el-form-item v-if="item.viewable !== false" :prop="item.prop" :label="item.label" :label-width="item.labelWidth" :size="item.size" > <template #default> <!-- 自定义列的内容slot改名为以属性名称命名的slot --> <slot :name="item.prop + 'View'"> <el-date-picker v-if="item.type === 'daterange'" v-model="form[item.prop]" type="daterange" range-separator="~" start-placeholder="开始日期" end-placeholder="结束日期" disabled :value-format=" item.valueFormat || 'YYYY-MM-DD' " > </el-date-picker> <el-date-picker v-else-if=" item.type === 'datetimerange' " disabled v-model="form[item.prop]" type="datetimerange" range-separator="~" start-placeholder="开始时间" end-placeholder="结束时间" :value-format=" item.valueFormat || 'YYYY-MM-DD hh:mm:ss' " > </el-date-picker> <el-checkbox v-else-if="item.type === 'checkbox'" v-model="form[item.prop]" :label="form[item.prop]" style="width: 220px" disabled ></el-checkbox> <el-cascader v-else-if=" item.type === 'select' || item.type === 'multiselect' || item.type === 'cascader' " v-bind="item.dict" v-model="form[item.prop]" style="width: 220px" disabled ></el-cascader> <el-input v-else clearable autosize placeholder="请输入..." style="width: 220px" v-bind="item" :type="item.type || 'text'" :show-password=" item.type === 'password' " v-model="form[item.prop]" disabled ></el-input> </slot> </template> <template #label="scope"> <!-- 自定义label的内容slot改名为以属性名开头的slot --> <slot :name="item.prop + 'Label'" :key="scope.key" :label="scope.label" > {{ scope.label }} </slot> </template> </el-form-item> </template> </slot> </template> </el-form> <template #footer> <span class="dialog-footer"> <el-button @click="dialogViewFormVisible = false"> 关闭 </el-button> </span> </template> </el-dialog> <!-- 编辑 --> <el-dialog v-model="dialogEditFormVisible" :title="editTitle" @open="handleEditOpen" @opened="handleEditOpened" @close="handleEditClose" @closed="handleEditClosed" > <template #title> <el-icon><Edit /></el-icon> <span>{{ editTitle }}</span> </template> <el-form ref="editFormRef" :model="form" :rules="rules" :inline="inline" :label-position="labelPosition" :label-width="labelWidth" :label-suffix="labelSuffix" :hide-required-asterisk="hideRequiredAsterisk" :show-message="showMessage" :inline-message="inlineMessage" :status-icon="statusIcon" :validate-on-rule-change="validateOnRuleChange" :size="size" :disabled="formDisabled" :validate="validate" :validateField="validateField" :resetFields="resetFields" :scrollToField="scrollToField" :clearValidate="clearValidate" @validate="formValidate" > <template #default> <slot name="editForm"> <template v-for="(item, index) in editColumns" :key="index" > <el-form-item v-if="item.editable" :prop="item.prop" :label="item.label" :label-width="item.labelWidth" :required="item.required" :rules="item.rules" :error="item.error" :show-message="item.showMessage" :inline-message="item.inlineMessage" :size="item.size" :resetField="item.resetField" :clearValidate="item.clearValidate" > <template #default> <!-- 自定义列的内容slot改名为以属性名称命名的slot --> <slot :name="item.prop + 'Edit'"> <el-date-picker v-if="item.type === 'daterange'" clearable v-model="form[item.prop]" type="daterange" range-separator="~" start-placeholder="开始日期" end-placeholder="结束日期" :value-format=" item.valueFormat || 'YYYY-MM-DD' " > </el-date-picker> <el-date-picker v-else-if=" item.type === 'datetimerange' " clearable v-model="form[item.prop]" type="datetimerange" range-separator="~" start-placeholder="开始时间" end-placeholder="结束时间" :value-format=" item.valueFormat || 'YYYY-MM-DD hh:mm:ss' " > </el-date-picker> <el-checkbox v-else-if="item.type === 'checkbox'" clearable v-model="form[item.prop]" :label="form[item.prop]" style="width: 220px" ></el-checkbox> <el-cascader v-else-if=" item.type === 'select' || item.type === 'multiselect' || item.type === 'cascader' " clearable filterable v-bind="item.dict" style="width: 220px" v-model="form[item.prop]" ></el-cascader> <el-input v-else clearable autosize placeholder="请输入..." style="width: 220px" v-bind="item" :type="item.type || 'text'" :show-password=" item.type === 'password' " v-model="form[item.prop]" ></el-input> </slot> </template> <template #label="scope"> <!-- 自定义label的内容slot改名为以属性名开头的slot --> <slot :name="item.prop + 'Label'" :key="scope.key" :label="scope.label" > {{ scope.label }} </slot> </template> <template #error="scope"> <!-- 自定义错误slot改名为以属性名开头的slot --> <slot :name="item.prop + 'Error'" :error="scope.error" > <div class="el-form-item__error"> {{ scope.error }} </div> </slot> </template> </el-form-item> </template> </slot> </template> </el-form> <template #footer> <span class="dialog-footer"> <el-button type="primary" @click="handleEditSure"> 确认 </el-button> <el-button @click="dialogEditFormVisible = false"> 取消 </el-button> </span> </template> </el-dialog> <!-- 新增 --> <el-dialog v-model="dialogAddFormVisible" :title="addTitle" @open="handleAddOpen" @opened="handleAddOpened" @close="handleAddClose" @closed="handleAddClosed" > <template #title> <el-icon><Plus /></el-icon> <span>{{ addTitle }}</span> </template> <el-form ref="addFormRef" :model="form" :rules="rules" :inline="inline" :label-position="labelPosition" :label-width="labelWidth" :label-suffix="labelSuffix" :hide-required-asterisk="hideRequiredAsterisk" :show-message="showMessage" :inline-message="inlineMessage" :status-icon="statusIcon" :validate-on-rule-change="validateOnRuleChange" :size="size" :disabled="formDisabled" :validate="validate" :validateField="validateField" :resetFields="resetFields" :scrollToField="scrollToField" :clearValidate="clearValidate" @validate="formValidate" > <template #default> <slot name="addForm"> <template v-for="(item, index) in addColumns" :key="index" > <el-form-item v-if="item.addable" :prop="item.prop" :label="item.label" :label-width="item.labelWidth" :required="item.required" :rules="item.rules" :error="item.error" :show-message="item.showMessage" :inline-message="item.inlineMessage" :size="item.size" :resetField="item.resetField" :clearValidate="item.clearValidate" > <template #default> <!-- 自定义列的内容slot改名为以属性名称命名的slot --> <slot :name="item.prop + 'Add'"> <el-date-picker v-if="item.type === 'daterange'" v-model="form[item.prop]" type="daterange" range-separator="~" start-placeholder="开始日期" end-placeholder="结束日期" :value-format=" item.valueFormat || 'YYYY-MM-DD' " clearable > </el-date-picker> <el-date-picker v-else-if=" item.type === 'datetimerange' " v-model="form[item.prop]" type="datetimerange" range-separator="~" start-placeholder="开始时间" end-placeholder="结束时间" :value-format=" item.valueFormat || 'YYYY-MM-DD hh:mm:ss' " clearable > </el-date-picker> <el-checkbox v-else-if="item.type === 'checkbox'" v-model="form[item.prop]" :label="form[item.prop]" style="width: 220px" clearable ></el-checkbox> <el-cascader v-else-if=" item.type === 'select' || item.type === 'multiselect' || item.type === 'cascader' " clearable filterable style="width: 220px" v-bind="item.dict" v-model="form[item.prop]" ></el-cascader> <el-input v-else clearable autosize placeholder="请输入..." style="width: 220px" v-bind="item" :type="item.type || 'text'" :show-password=" item.type === 'password' " v-model="form[item.prop]" ></el-input> </slot> </template> <template #label="scope"> <!-- 自定义label的内容slot改名为以属性名开头的slot --> <slot :name="item.prop + 'Label'" :key="scope.key" :label="scope.label" > {{ scope.label }} </slot> </template> <template #error="scope"> <!-- 自定义错误slot改名为以属性名开头的slot --> <slot :name="item.prop + 'Error'" :error="scope.error" > <div class="el-form-item__error"> {{ scope.error }} </div> </slot> </template> </el-form-item> </template> </slot> </template> </el-form> <template #footer> <span class="dialog-footer"> <el-button type="primary" @click="handleAddSure"> 确认 </el-button> <el-button @click="dialogAddFormVisible = false"> 取消 </el-button> </span> </template> </el-dialog> <!-- 删除 --> <el-dialog v-model="dialogDeleteVisible" :title="deleteTitle" width="30%" > <template #title> <el-icon><Delete /></el-icon> <span>{{ deleteTitle }}</span> </template> <span>确认删除吗?</span> <template #footer> <span class="dialog-footer"> <el-button type="primary" @click="handleDeleteSure"> 确认 </el-button> <el-button @click="dialogDeleteVisible = false"> 取消 </el-button> </span> </template> </el-dialog> </div> </template> <script> export default { name: "ve-curd", emits: [ "update:currentPage", "update:pageSize", "update:ascs", "update:descs", "update:filters", "searchChange", "sizeChange", "currentChange", "sortChange", "filterChange", "viewOpen", "viewOpened", "viewClose", "viewClosed", "addOpen", "addOpened", "addClose", "addClosed", "addSure", "editOpen", "editOpened", "editClose", "editClosed", "editSure", "deleteSure", ], props: { curdType: { type: String, default: "table", // tree }, props: Object, // 表格数据 data: Array, // 编辑表单 editTitle: { type: String, default: "编辑", }, // 新增表单 addTitle: { type: String, default: "新增", }, // 查看表单 viewTitle: { type: String, default: "查看", }, // 查看表单 deleteTitle: { type: String, default: "删除", }, // 详情表单 detailTitle: { type: String, default: "详情", }, // 查询表单相关 rules: Object, // el-form的rules inline: { type: Boolean, default: true, }, formDisabled: Boolean, labelPosition: String, labelWidth: { type: [String, Number], default: "160px", }, labelSuffix: String, hideRequiredAsterisk: Boolean, showMessage: { type: Boolean, default: true, }, inlineMessage: Boolean, statusIcon: Boolean, validateOnRuleChange: Boolean, size: String, validate: Function, validateField: Function, resetFields: Function, scrollToField: Function, clearValidate: Function, // 表属性全部支持el-table的原始属性、方法、事件和插槽 只是将 el改为ve 直接通过 v-bind="$attr" 传给el-table组件 permission: { type: Object, default: () => {}, }, // 表列属性 在ve-table上通过:columns 绑定 el-table-column的所有支持的属性、方法和插槽等,也新增了一些crud相关的属性 columns: { type: Array, default: () => [], }, // 分页 和原始el-pagination属性使用没什么改变,只是全部绑定在ve-table上面 pageSizes: { type: Array, default: () => [10, 20, 50, 100, 200, 500], }, hideOnSinglePage: { type: Boolean, default: true, }, small: Boolean, background: Boolean, total: Number, pageCount: Number, pagerCount: Number, // pageSize: { // type: Number, // default: 10, // }, // currentPage: { // type: Number, // default: 1, // }, popperClass: String, prevText: String, nextText: String, paginationDisabled: Boolean, defaultCurrentPage: { type: Number, default: 1, }, defaultPageSize: Number, layout: { type: String, default: "total, sizes, prev, pager, next, jumper", }, }, }; </script> <script setup> import { getCurrentInstance, onMounted, reactive, ref, unref, watch, } from "vue"; import { Search, Plus, Delete, View, Edit, Expand, Folder, Document, } from "@element-plus/icons-vue"; const { proxy } = getCurrentInstance(); //获取上下文实例,ctx=vue2的this const form = ref({}); const queryFormRef = ref(); const addFormRef = ref(); const editFormRef = ref(); const dialogDeleteVisible = ref(false); const dialogViewFormVisible = ref(false); const dialogEditFormVisible = ref(false); const dialogAddFormVisible = ref(false); const searchShow = ref(true); const drawerColumnShow = ref(false); const drawerColumnTree = ref(null); const treeQuery = ref(""); // eslint-disable-next-line no-undef const treeRef = ref(); const onTreeQueryChanged = (treeQuery) => { if (treeRef.value && treeRef.value.filter) { treeRef.value.filter(treeQuery); } }; const filterHandle = (treeQuery, node) => { if (!treeQuery) { return true; } let label = node[proxy.props["label"] || "label"]; if (typeof proxy.filterMethod === "function") { return proxy.filterMethod(treeQuery, node); } if (label) { return label.indexOf(treeQuery) !== -1; } return false; }; const handleSearchShow = () => { searchShow.value = !searchShow.value; }; const drawerTreeChange = (val, node) => { val.hide = !node; }; const handleSizeChange = (val) => { // this.$attr.onSizeChange(val) // OK proxy.$emit("update:pageSize", val); // OK proxy.$emit("sizeChange", val); // OK }; const handleCurrentChange = (val) => { proxy.$emit("update:currentPage", val); // OK proxy.$emit("currentChange", val); // OK }; const formValidate = (prop) => { proxy.$emit("validate", prop); }; const tableColumns = ref( proxy.columns.map((column) => { let col = {}; for (const k in column) { col[k] = column[k]; } if ( col.type !== "selection" && col.type !== "index" && col.type !== "expand" ) { col.rawFilters = !!col.filters; col.filterMethod = col.filterMethod || function (value, row, column) { const property = column["property"] || column["prop"]; return row[property] === value; }; } return col; }) ); const formColumns = (columns = [], res = []) => { return columns.reduce((cols, col) => { if (col.children instanceof Array) { formColumns(col.children, cols); } col.showMessage = !!col.showMessage; cols.push(col); return cols; }, res); }; const searchColumns = reactive( formColumns(proxy.columns).filter((col) => col.search === true) ); const viewColumns = reactive( formColumns(proxy.columns).filter((col) => col.viewable === true) ); const editColumns = reactive( formColumns(proxy.columns).filter((col) => col.editable === true) ); const addColumns = reactive( formColumns(proxy.columns).filter((col) => col.addable === true) ); const handleSearch = async function () { const form = unref(queryFormRef); if (!form) { return; } try { let valid = await form.validate(); if (valid) { searchChange(); } } catch (err) { console.log("err", err); } }; const resetForm = function () { const form = unref(queryFormRef); form.resetFields(); }; const ascs = ref([]); const descs = ref([]); const handleSortChange = ({ column, prop, order }) => { let idx = ascs.value.indexOf(prop); if (idx > -1) { ascs.value.splice(idx, 1); } idx = descs.value.indexOf(prop); if (idx > -1) { descs.value.splice(idx, 1); } if ("ascending" === order) { ascs.value.push(prop); } else if ("descending" === order) { descs.value.push(prop); } proxy.$emit("update:ascs", ascs.value); proxy.$emit("update:descs", descs.value); proxy.$emit("sortChange", { column, prop, order }); }; const handleFilterChange = (filters) => { proxy.$emit("update:filters", filters); proxy.$emit("filterChange", filters); }; const searchChange = () => { proxy.$emit("searchChange"); // OK }; // 查看 const handleTreeView = (node, data) => { dialogViewFormVisible.value = true; form.value = JSON.parse(JSON.stringify(data)); }; const handleView = (row, index) => { dialogViewFormVisible.value = true; form.value = row; }; const handleViewOpen = () => { proxy.$emit("viewOpen", form.value); // OK }; const handleViewOpened = () => { proxy.$emit("viewOpened", form.value); // OK }; const handleViewClose = () => { proxy.$emit("viewClose", form.value); // OK }; const handleViewClosed = () => { proxy.$emit("viewClosed", form.value); // OK }; // 编辑 const handleTreeEdit = (node, data) => { dialogEditFormVisible.value = true; form.value = JSON.parse(JSON.stringify(data)); }; const handleEdit = (row, index) => { dialogEditFormVisible.value = true; form.value = JSON.parse(JSON.stringify(row)); }; const handleEditSure = async () => { const ef = unref(editFormRef); if (!ef) { return; } try { let valid = await ef.validate(); if (valid) { let value = form.value; let find = false; for (const k in value) { if (value[k] instanceof Array) { let columns = formColumns(proxy.columns); for (let col of columns) { if (col.type === "select") { value[k] = value[k][0]; find = true; break; } if (col.type === "multiselect") { value[k] = value[k].join(); find = true; break; } } if (find) { break; } } } proxy.$emit("editSure", value); // OK dialogEditFormVisible.value = false; } } catch (err) { console.log("err", err); } }; const handleEditOpen = () => { proxy.$emit("editOpen", form.value); // OK }; const handleEditOpened = () => { proxy.$emit("editOpened", form.value); // OK }; const handleEditClose = () => { proxy.$emit("editClose", form.value); // OK }; const handleEditClosed = () => { proxy.$emit("editClosed", form.value); // OK }; // 新增 const handleTreeAdd = (node, data = {}) => { dialogAddFormVisible.value = true; form.value = JSON.parse(JSON.stringify(data)); }; const handleAdd = () => { dialogAddFormVisible.value = true; form.value = {}; }; const handleAddSure = async () => { const ef = unref(addFormRef); if (!ef) { return; } try { let valid = await ef.validate(); if (valid) { let value = form.value; let find = false; for (const k in value) { if (value[k] instanceof Array) { let columns = formColumns(proxy.columns); for (let col of columns) { if (col.type === "select") { value[k] = value[k][0]; find = true; break; } if (col.type === "multiselect") { value[k] = value[k].join(); find = true; break; } } if (find) { break; } } } proxy.$emit("addSure", value); // OK dialogAddFormVisible.value = false; } } catch (err) { console.log("err", err); } }; const handleAddOpen = () => { proxy.$emit("addOpen", form.value); // OK }; const handleAddOpened = () => { proxy.$emit("addOpened", form.value); // OK }; const handleAddClose = () => { proxy.$emit("addClose", form.value); // OK }; const handleAddClosed = () => { proxy.$emit("addClosed", form.value); // OK }; //删除 const handleTreeDelete = (node, data) => { dialogDeleteVisible.value = true; form.value = JSON.parse(JSON.stringify(data)); }; const handleDelete = (row, index) => { dialogDeleteVisible.value = true; form.value = row; }; const handleDeleteSure = () => { proxy.$emit("deleteSure", form.value); // OK dialogDeleteVisible.value = false; }; watch( () => proxy.data, (n) => { //直接监听 let cols = tableColumns.value; for (let i = 0; i < cols.length; i++) { let col = cols[i]; col.dict = col.dict || {}; col.dict.props = Object.assign( { label: "label", value: "value", }, col.dict.props ); if (col.rawFilters) { col.filters = col.filters.map((v) => { return { text: v.text || v.label, label: v.label || v.text, value: v.value, }; }); col.dict.options = col.dict.options || col.filters; continue; } if (col.type === "select") { if (col.dict instanceof Array) { col.dict.forEach((v, i, a) => { if ( typeof v === "number" || typeof v === "string" || typeof v === "number" || v instanceof Date ) { a[i] = { text: v, label: v, value: v }; } }); col.filters = col.dict; col.dict = { options: col.dict }; } else if (typeof col.dict === "object") { let method = col.dict.method || "get"; if (col.dict.options instanceof Array) { col.filters = col.dict.options.map((v) => { return { text: v[col.dict.props.label], label: v[col.dict.props.label], value: v[col.dict.props.value], }; }); tableColumns.value = cols; } else if (col.dict.url) { proxy.$axios[method](col.dict.url, {}).then((res) => { let data = res.data; if ( !(data instanceof Array) && data.data instanceof Array ) { data = data.data; } else { data = []; } data = data.map((v) => { if ( typeof v === "number" || typeof v === "string" || typeof v === "number" || v instanceof Date ) { return { text: v, label: v, value: v }; } return { text: v[col.dict.props.label], label: v[col.dict.props.label], value: v[col.dict.props.value], }; }); col.dict.options = data; col.filters = data; tableColumns.value = cols; }); } } } else { col.filters = n.reduce( (a, v) => { if (!a.k[v[col.prop]]) { a.k[v[col.prop]] = true; a.a.push({ text: v[col.prop], label: v[col.prop], value: v[col.prop], }); } return a; }, { k: {}, a: [] } ).a; col.dict.options = col.filters; } } tableColumns.value = cols; } ); const initDrawerColumnTree = () => { drawerColumnShow.value = true; setTimeout(() => { if (drawerColumnTree.value) { tableColumns.value.forEach((c) => { drawerColumnTree.value.setChecked(c, c.hide !== true, false); //反选 }); } }, 0); }; const getLabel = (value, column) => { if (!column) { return value; } if (column.dict && column.dict.options instanceof Array) { for (let opt of column.dict.options) { if (opt.value === value) { return opt.label || opt.text || value; } } } return value; }; onMounted(() => { console.log("proxy", proxy); }); </script> <style scoped> .el-tree { margin-top: 10px; } .el-tree-node { font-size: 34px; } .el-tree-node__content { margin: 5px 0; } </style>