基于vue3和element-plus封装的curd组件,支持el-table和el-tree

vue

本文连接: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>

以上是 基于vue3和element-plus封装的curd组件,支持el-table和el-tree 的全部内容, 来源链接: utcz.com/z/379600.html

回到顶部