基于vue3和element-plus封装的curd组件,支持el-table和el-tree
本文连接: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