基於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>