

中高端软件定制开发服务商

13245491521 13245491521
B端开发化神期--这个Vue3列表设计能成为你心中的白月光吗? 1.前言道友能来到此处,证明你我有缘,既然如此,我想送你一场造化! 本系列文章主要分享个人在多年中后台前端开发中,对于表单与列表封装的一些探索以及实践.本系列分享是基于vue3+element-plus,设计方案可能无法满足所有人的需求,但是可以解决大部分人业务中的开发需求.主要还是希望通过分享能够得到一些新的反馈与启发,进一步完善改进,分享中夯实己身,在反馈中不断成长。 时间原因文章会不定期更新,有空就写.下面先展示一下一个完整的常见的表单+表格集成的列表页面开发的场景,然后再拆解ElTable表格的二次封装实现封装. 示例代码展示: DemoService.tsexport function queryPlatformList() { const platformList = [ { name: "淘宝", code: "taobao" }, { name: "京东", code: "jd" }, { name: "抖音", code: "douyin" }, ]; return platformList; } const dataList: any[] = [ { id: 1, channelType: "sms", channelName: "阿里短信通知", platforms: queryPlatformList().filter((item) = item.code !== "taobao"), status: 1, createTime: "2021-09-07 00:52:15", updateTime: "2021-11-07 00:52:15", createBy: "vshen", updateBy: "vshen", ext: { url: "https://sms.aliyun.com", account: "vshen", password: "vshen57", sign: "signVhsen123124", }, }, { id: 2, channelType: "dingtalk", channelName: "预警消息钉钉通知", platforms: queryPlatformList().filter((item) = item.code !== "jingdong"), status: 1, createTime: "2021-11-10 00:52:15", updateTime: "2021-11-07 00:52:15", createBy: "vshen", updateBy: "vshen", ext: { accessType: "webhook", address: "https://dingtalk.aliyun.com", }, }, { id: 3, channelType: "email", channelName: "预警消息邮件通知", platforms: queryPlatformList().filter((item) = item.code !== "douyin"), status: 0, ext: { host: "https://smpt.aliyun.com", account: "vshen@qq.com", password: "vshen@360.com", }, createTime: "2021-11-07 00:52:15", updateTime: "2021-11-07 00:52:15", createBy: "vshen", updateBy: "vshen", }, ]; export function queryPage({ form }: any, pagenation: any) { return new Promise((resolve) = { let result: any[] = dataList; Object.keys(form).forEach((key) = { const value = form[key]; result = dataList.filter((item) = item[key] == value); }); resolve({ success: true, data: { list: result } }); }); } export function create(data: any = {}) { return new Promise((resolve) = { setTimeout(() = { dataList.push({ id: Date.now(), platform: [], ...data, }); resolve({ success: true, message: "创建成功!" }); }, 500); }); } export function update(data: any) { return new Promise((resolve) = { setTimeout(() = { const index = dataList.findIndex((item) = item.id == data.id); const target = dataList[index]; Object.keys(data).forEach((key) = { tarGET@[key] = data[key]; }); dataList.splice(index, 1, target); resolve({ success: true, message: "更新成功!" }); console.log("update", dataList); }, 500); }); } export function remove(id: number) { return new Promise((resolve) = { setTimeout(() = { const index = dataList.findIndex((item) = item.id == id); dataList.splice(index, 1); resolve({ success: true, message: "删除成功!" }); console.log("remove", dataList); }, 500); }); } FormDialog.ts(实现示例中的新增/编辑的动态表单)import{createFormDialog}from"@/components/Dialogs"; import{Toast}from"@/core/adaptor"; import*asDemoServicefrom"@/api/demo-service"; exportconstChannelEnum:any={ sms:"短信通知", dingtalk:"钉钉通知", email:"邮件通知", }; exportconstAccessTypeEnum:any={ webhook:"webhook", api:"api", }; constDingtalkVisiable=(formData:any)=formData.channelType=="dingtalk"; constDingtalkApiVisiable=(formData:any)={ return( DingtalkVisiable(formData)formData.accessType==AccessTypeEnum.api }; constDingtalkWebhookVisiable=(formData:any)={ return( DingtalkVisiable(formData)formData.accessType==AccessTypeEnum.webhook }; constDingTalkFormItems=[ { label:"接入方式", field:"accessType", visiable:DingtalkVisiable, uiType:"selector", props:{ options:AccessTypeEnum, }, }, { label:"webhHook地址", field:"address", required:true, visiable:DingtalkWebhookVisiable, uiType:"input", }, { label:"appKey", field:"appKey", visiable:DingtalkApiVisiable, uiType:"input", }, { label:"appSecret", field:"appSecret", visiable:DingtalkApiVisiable, uiType:"input", }, { label:"clientId", field:"clientId", visiable:DingtalkApiVisiable, uiType:"input", }, { label:"钉钉群ID", field:"chatId", visiable:DingtalkApiVisiable, uiType:"input", }, ]; /******* 支持的规则描述 interfaceRuleType{ equals?:string; not?:string; in?:string; notIn?:string; includes?:string|string[]; excludes?:string|string[]; empty?:boolean; lt?:number; lte?:number; gt?:number; gte?:number; } * * *********/ constSmsVisiable={ channelType:{ equals:"sms", }, }; constSmsFormItems=[ { label:"消息推送地址", field:"url", visiable:SmsVisiable, uiType:"input", }, { label:"账号", field:"account", visiable:SmsVisiable, uiType:"input", }, { label:"密码", field:"password", visiable:SmsVisiable, uiType:"input", }, { label:"签名", field:"sign", initValue:"signature", visiable:SmsVisiable, uiType:"input", }, ]; constEmailVisiable=(formData:any)=formData.channelType=="email"; constEmailFormItems=[ { label:"smtp服务器地址", field:"host", visiable:EmailVisiable, uiType:"input", }, { label:"邮箱账号", field:"account", visiable:EmailVisiable, uiType:"input", }, { label:"邮箱密码", field:"password", visiable:EmailVisiable, uiType:"input", }, ]; functioncreateFormItems(isEditMode:boolean,extJson:any=null){ return[ { label:"渠道名称", field:"channelName", uiType:"input", required:true, }, { label:"渠道类型", field:"channelType", required:true, uiType:"selector", disabled:isEditMode, props:{ options:ChannelEnum, }, }, ...DingTalkFormItems, ...SmsFormItems, ...EmailFormItems, { label:"应用于平台", field:"platforms", required:true, uiType:"selector", props:{ multiple:true, options:()=DemoService.queryPlatformList(), }, }, } exportasyncfunctioncreateOrUpdateChannel(row:any,table:any){ constisEditMode=!!row; letrowData=null; if(isEditMode){ rowData={ ...row, ...row.ext, platforms:row.platforms.map((item:any)=item.code), } constdialogInsatcne=createFormDialog({ dialogProps:{ title:isEditMode?"编辑渠道":"新增渠道", }, formProps:{ labelWidth:130, primaryKey:"id",//编辑操作需要传给后端用来更新的主键,不传默认为id }, formItems:createFormItems(isEditMode,rowData), dialogInsatcne.open(rowData) .onConfirm((formData:any)={ /**** *只有表单所有必填字段校验通过才会调用此回调函数 *formData只包含可视的字段与primaryKey,保证数据干净 ****/ constaction=!isEditMode?"create":"update"; DemoService[action](formData).then(({success,errorMsg})={ if(!success){ Toast.error(errorMsg); return; } Toast.success(errorMsg); table.refresh(); dialogInsatcne.close(); }) .onClose(()={}); } demo-list-page.vuetemplate list-pagev-bind="table" template#expand="{row}" el-table:data="row.platforms"borderstripestyle="padding:10px;width:100%" el-table-columnlabel="平台名称"prop="name"/ el-table-columnlabel="平台编码"prop="code"/ /el-table /template template#status="{row}" el-tag:type="row.status==1?'info':'danger'"{{statusEnum[row.status]}} /el-tag /template /list-page /template scriptsetuplang="ts" import{Toast,Dialog}from"@/core/adaptor"; import*asdemoServicefrom"@/api/demo-service"; import{createOrUpdateChannel,ChannelEnum}from"./formDialog"; conststatusEnum:any={ 0:"禁用", 1:"启用", }; consttable=reactive({ //支持el-table的所有属性 props:{}, //支持el-table的所有事件 events:{}, loader:(queryForm,pagenation):any=demoService.queryPage(queryForm,pagenation), //过滤条件选项 filterItems:[ { label:"渠道类型", field:"channelType", uiType:"selector", props:{options:ChannelEnum}, }, { label:"启用状态", field:"status", uiType:"selector", props:{options:statusEnum}, }, { label:"创建时间", field:["stratTime","endTime"], uiType:"dateTimePicker", props:{ type:"daterange", }, }, ], columns:[ {type:"selection",label:"全选"}, {type:"index",label:"序号"}, {type:"expand",label:"使用平台"}, {label:"渠道名称",key:"channelName"}, { label:"通知方式", key:"channelType", formatter:(row)=ChannelEnum[row.channelType], }, { label:"密钥", text:"查看密钥", click:()={ Toast("查看密钥"); }, }, {label:"启用状态",slot:"status"}, {label:"创建时间",key:"createTime"}, {label:"创建人",key:"createBy"}, {label:"更新时间",key:"updateTime"}, {label:"更新人",key:"updateBy"}, ], toolbar:[ { text:"新增消息渠道", click:(table:any,searchForm:any)=createOrUpdateChannel(null,table), }, { text:"批量删除", click:(table:any)={ constrows=table.instance.getSelectionRows(); if(rows.length==0){ Toast.info(`请先选择要删除的数据`); return; } Dialog.confirm( `确定要删除消息渠道配置${rows.map((row)=row.channelName)}吗?` ).then((res)={ if(res!="confirm"){ return; } table.refresh(); }, }, ], actions:[ { text:"编辑", props:{type:"warning"}, click:({row}:any,table:any)=createOrUpdateChannel(row,table), }, { text:(row)=(row.status==1?"禁用":"启用"), props:(row)=(row.status==1?{type:"danger"}:{type:"success"}), confirm:(row)=`确定${row.status==1?"禁用":"启用"}${row.channelName}吗?`, click:({row}:any,table:any,searchForm:any)={ demoService .update({id:row.id,status:row.status==1?0:1}) .then(({success,message})={ constaction=success?"success":"error"; Toast[action](message); successtable.refresh(); }, }, ], }); /script 至于此种开发方式对开发效率有没有提升,看完上面示例的代码后读者朋友可以尝试实现图示中的效果,然后从时间耗费、代码量、拓展性与可维护性等多个维度做下对比,本示例开发连同构造数据模拟花了差不多2h,因为思考示例中如何才能将封装的东西更多地展现出来,也稍微花了点时间。 社区中确实看到有很不少人对这种配置式开发嗤之以鼻,但是在我看来至少有以下几个优点: 统一了项目中的列表页开发规范,无论谁开发都可以保证每一个列表页面其他人都可以看得懂,改得动。在前端人手不足情况下,即使后端不会css跟布局,只要给与相关文档看一下,也能动手写出一样的列表页面开发(后端开发的道友对不住了,哈哈哈)没有一个功能代码需要反复横跳查看的,每一个方法逻辑都可以很好很清晰的剥离与替换,解耦业务逻辑。例如示例中的新增与编辑操作,将相关业务逻辑内聚,从页面代码中剥离出来单独维护,需求变动时任何方法都可以很方便地直接拿掉或者重写,无需担心会影响其他业务代码。2.代码拆解接下来我们进入主题,拆解下(ListPage.vue)这个页面的组件分封装。对于页面展示的各个部分,在代码封装设计上我们按照图示中圈出来的各个部分来做封装设计。 image.png代码组织如下: image.png1. 列表页面 (ListPage.vue) 整个列表列页面在设计上主要由SearchForm、Toolbar、Pagenation、ElTablePlus、TableCustomSetting几个部分组合而成,整体代码量不多,完整代码如下: template divref="listPageRef"class="list-page" !--搜索框-- SearchFormv-show="props.filterItems?.length0"v-model:height="searchFormHeight":filterItems="props.filterItems" @search="diapatchSearch" /SearchForm !---- el-rowclass="table-grid"justify="start"flex !--表格操作-- divclass="toolbar-actions" el-buttonv-for="actioninprops.toolbar" v-bind="Object.assign({size:'small',type:'primary'},action.props)" @click="()=action.click(tableInstance,{})" el-iconstyle="vertical-align:middle"v-if="action.propsaction.props.icon" /el-icon span{{action.text}}/span /el-button el-buttontype="warning"size="small"@click="refreshTableData(searchFormModel)" el-iconstyle="vertical-align:middle" Refresh/ /el-icon /el-button el-buttontype="info"size="small"@click.stop="tableSettingDialog.open()" el-iconstyle="vertical-align:middle" Setting/ /el-icon /el-button el-buttontype="success"size="small"@click="requestFullScreen.toggle()" el-iconstyle="vertical-align:middle" FullScreen/ /el-icon /el-button /div !--表格主体-- el-table-plusref="tableInstance":data="tableData.list":is-loading="tableData.isLoading":columns="tableColumns" :tableHeight="tableHeight":props="props.props":events="props.props" v-bind="Object.assign($attrs.props||{},{})"@refresh="()=refreshTableData(searchFormModel)" templatev-for="columnintableColumns.filter((col)=col.slot)"#[column.slot]="{row,col,index}" slot:name="column.slot":row="row":col="col":index="index"/slot /template /el-table-plus !--分页-- Pagenationtype="custom":pagenation="searchFormModel.pagenation":total="tableData.total" @change="onPagenationChange"v-model:height="pagenationHeight" /Pagenation /el-row TableCustomSettingDialogref="tableSettingDialog"v-model:columns="tableColumns"@refresh-column="refreshColumn" @reset="resetColumns"/ /div /template scriptsetuplang="ts" importSearchFormfrom"@/components/Forms/SearchForm.vue"; importPagenationfrom"./components/Pagenation.vue"; importElTablePlusfrom"@/components/Table/Table.vue"; importTableCustomSettingDialogfrom"./components/TableSettingDialog.vue"; import{FullScreen,Refresh,Setting}from"@element-plus/icons-vue"; import{useTable,ISearchForm}from"@/components/Table/useTable"; import{useColumn}from"@/components/Table/tableColumns"; import{useTableSetting}from"@/components/Table/tableCustomSetting"; import{useFullscreen}from"@vueuse/core"; exportinterfaceAction{ text:string|Function; click:(row:any,table:any)={}; props: } exportinterfaceIProps{ loader:Function|Arrayany filterItems?:any[]; columns:any[]; actions?:Action[]; toolbar?:Action[]; tableHeight?:string; props?: events?: } constprops=withDefaults(definePropsIProps(),{ props:{}, events:{}, }); /**表格数据获取与刷新逻辑**/ constsearchFormModel=reactiveISearchForm({ form:{}, pagenation:{pageNum:1,pageSize:20}, }); const{tableData,refreshTableData}=useTable( props.loader, props.filterItems?.length0?null:searchFormModel ); constonPagenationChange=({pageNum,pageSize})={ searchFormModel.pagenation.pageNum=pageNum; searchFormModel.pagenation.pageSize=pageSize; refreshTableData(searchFormModel); }; constdiapatchSearch=(form)={ searchFormModel.form=form; searchFormModel.pagenation.pageNum= refreshTableData(searchFormModel); }; consttableInstance=ref(null); consttableSettingDialog=ref(null); const{tableColumns,updateTableColumns}=useColumn(props.columns,props.actions); const{refreshColumn,resetColumns}=useTableSetting( tableInstance, updateTableColumns ); /***表格动态高度计算***/ constlistPageRef=refHTMLElement(null); constsearchFormHeight=ref(0); constpagenationHeight=ref(0); consttableHeight=ref(0); constupdateTableHeight=()={ tableHeight.value= listPageRef.value?.clientHeight- searchFormHeight.value- pagenationHeight.value- }; letcancelWatch=null; onMounted(()={ cancelWatch=watchEffect(()=updateTableHeight()); window.addEventListener("resize",()=nextTick(()=updateTableHeight())); }); onUnmounted(()={ cancelWatch(); window.removeEventListener("resize",()=nextTick(()=updateTableHeight())); }); constrequestFullScreen=useFullscreen(listPageRef.value); /script 2. 列表数据请求(useTable.ts) 在实际开发过程中列表数据源可能来源于各个地方,可能是接口,也可能是手动枚举的数据。设计上我们支持传入数组与方法,这一层主要是对数据的输入=输出做归一化处理,减少应用时对数据格式的心智负担。 具体可以参考下面完整的代码: import{isArray,isFunction}from"@vue/shared"; exportinterfaceIPagination{ pageSize:number; pageNum:number; } exportinterfaceISearchForm{ form?: pagenation:IPagination; } exportinterfaceTableData{ list:any[]; total:number; isLoading:boolean; } exportfunctionuseTable( dataLoader:Function|any[], searchForm?:ISearchForm ){ consttableRef=refHTMLElement consttableData=reactiveTableData({ list:[], total:0, isLoading:false, asyncfunctionrequestTableData(dataLoader:any,searchForm:ISearchForm){ tableData.isLoading=true; if(!isArray(dataLoader)!isFunction(dataLoader)){ console.error("----表格数据必须是方法或者数组----"); return; } letpromiseLoader=(searchForm)= Promise.resolve( isArray(dataLoader)?dataLoader:dataLoader(searchForm) try{ constresult=awaitpromiseLoader(searchForm); if(Array.isArray(result)){ tableData.list=result; tableData.total=result.length; tableData.isLoading=false; return; } const{success,data,rows}:any=result; if(!success){ tableData.list= tableData.total=0; tableData.isLoading=false; return; } tableData.list=Array.isArray(data)?data:data.list||rows; tableData.total=data.total||tableData.list.length; }catch(error){ console.error(error); }finally{ tableData.isLoading=false; } } functionrefreshTableData(searchFormModel={}){ requestTableData( dataLoader, Object.assign({},searchFormModel,searchForm) } if(searchForm){ requestTableData(dataLoader,searchForm); } return{ tableRef, tableData, listData, requestTableData, refreshTableData, } 3. 列表列配置二次处理 (tableColumns.ts) 对列配置单独提取出来做二次处理,可以方便我们做一些中间的转换与列更新的操作的控制。对于业务开发中的一些开发拓展也很方便。 (以我自身经历的一个业务场景来说,某项目需要支持私有化部署跟saas环境部署,但是有多个页面在不同环境需要展示不同的字段。按照常规操作需要一个个页面去读取环境变量来做控制,操作起来就很复杂。我采用的就是在列配置上拓展一个环境支持的字段,然后在tableColumns引入环境变量做统一的过滤处理) 此外,这一层可以支持对多种UI框架的table组件进行支持。例如列属性字段,对应到不同框架中有的可能是prop,有的是property,有的是field。 import{IColumnSetting}from"@/api/table-setting-service"; import{isFunction}from"@vue/shared"; exporttypeFixedType="left"|"right"|"none"|boolean; exporttypeElColumnType="selection"|"index"|"expand"; exporttypeCustomColumnType="text"|"action"; exporttypeColumnType=ElColumnType|CustomColumnType; exporttypeAction={ text:Functionstring; click:Function; }{ [key:string]:string; }; exportinterfaceTColumn{ label:string;//列标题可以是函数或字符串,根据需要在页面上显示在列 key?:string; property?:string;//列的属性,如果没有指定,则使用列名称如果是函数 slot?:string; align?:string; width?:number|string;//列宽度可选参数,默认为100可以是整数或浮点数,但不 minWidth?:number|string;//最小列宽度可选参数,默认为10可以是整数或浮点 fixed?:FixedType;//列宽对齐方式leftrightnone默认为left可选参数,表示对齐方 type?:string; actions?:any[]; visiable?:boolean; click?:Function; text?:Function|string; } exporttypeTableType="VXE-TABLE"|"EL-TABLE"; exporttypeTColumnConfig= exportconstactionColumn:TColumn={ label:"操作", fixed:"right", type:"action", visiable:true, actions:[], }; exportconstcomputedActionName=(button:Action,row:TColumn)={ return!isFunction(button.text) ?button.text :computed(()=button.text(row)).value?.replace(/\"/g,""); }; consttableColumns=refArrayTColumn([]); exportconstspecificTypes=["selection","index","expand"]; constcalcColumnWidth=(columnsLength:number)={ if(columnsLength=6)return`${100/columnsLength}%`; return`${12}%`; }; constformatColumns=(columns:ArrayTColumn,actions:any[]=[])={ consthasAction=actions?.length actionColumn.actions=[...actions]; const_columns=hasAction?[...columns,actionColumn]:[...columns]; constnewColumns= for(letcolumnof_columns){ column=Object.assign({},column); if(column.visiable==false){ continue; } column.property=column.key||column.slot; column.align=column.align||"center"; column.visiable=true; column.width=column.width||"auto"||calcColumnWidth(_columns.length); if(specificTypes.includes(column.type)){ column.width=column.width|| } if(column.type==="expand"){ column.slot=column.slot||"expand"; } if(column.type==="action"){ column.minWidth= column.fixed="right"; } newColumns.push(column); } returnnewColumn; }; constupdateTableColumns=(columnSettings:IColumnSetting[])={ if(columnSettings.length==0)returnfalse; constcolumnSettingMap=newMap(); columnSettings.forEach((col)=columnSettingMap.set(col.field,col)); tableColumns.value=tableColumns.value.map((col)={ constcolSetting=columnSettingMap.get(col.key)|| Object.keys(colSetting).forEach((key)={ col[key]=colSetting[key]; return returntrue; }; exportfunctionuseColumn(columns:ArrayTColumn,actions:any[]){ tableColumns.value=formatColumns(columns,actions); console.log("tableColumns",tableColumns); return{ tableColumns, updateTableColumns, computedActionName, } 4. 列表组装(table.vue) 对el-table组件二次封装,首先我们要保证对原组件所有的方法与属性可以完全的支持,在不影响原组件的功能上增加拓展。这里用属性/事件透传,然后用v-bind,v-on分别做绑定即可实现。不清楚的道友可以看下官方的这两个指令。 在拓展上我们这里除了支持action,slot,还增加了一个click配置,这个主要针某个列展示的数据我们希望点击的时候可以进行跳转等操作。所有配置的支持都是根据平时业务开发中的真实场景来设计的。 看懂了下面的代码,可以根据自己的业务进行拓展支持。 template el-tableref="tableInstance":data="props.data":loading="props.isLoading"v-on="Object.assign({},$attrs.events)" v-bind="Object.assign( { tableLayout:'auto', maxHeight:`${props.tableHeight}px`, border:true, stripe:true, resizable:true, key:Date.now(),//不配置key会存在数据更新页面不更新 }, $attrs.props||{} ) " templatev-for="columninprops.columns" !--操作-- el-table-columnv-if="column.type=='action'"v-bind="column"#default="scope" templatev-for="buttonincolumn.actions" action-button:button="button":scope="scope"@click="()=button.click(scope,exposeObject)" /action-button /template /el-table-column el-table-columnv-else-if="isFunction(column.click)"v-bind="column" template#default="{row,col,index}" el-buttonv-bind="Object.assign({type:'primary',size:'small'},column.props||{})" @click="column.click(row,col,index)" {{ isFunction(column.text) ?column.text(row,col,index) :column.text||row[column.key] }} /el-button /template /el-table-column el-table-columnv-else-if="column.slot"v-bind="column" template#default="{row,col,$index}" slot:name="column.slot":row="row":col="col":index="$index":key="$index" /slot /template /el-table-column el-table-columnv-elsev-bind="column"/el-table-column /template /el-table /template scriptsetuplang="ts" import{TColumn,Action}from"./tableColumns"; import{isFunction}from"@vue/shared"; importActionButtonfrom"./ActionButton.vue"; import{TableInstance}from"element-plus"; import{toValue}from"vue"; exportinterfaceProps{ columns?:TColumn[]; actions?:Action[]; data?: isLoading:boolean; tableHeight:number; } constprops=withDefaults(definePropsProps(),{ columns:()=[], actions:()=[], data:()=[], tableHeight:200, isLoading:false, }); constemit=defineEmits(["refresh"]); constrefresh=()={ emit("refresh"); }; consttableInstance=refTableInstance constexposeObject:any=reactive({ instance:tableInstance, refresh, selectionRows:toValue(computed(()=tableInstance.value?.getSelectionRows())), }); defineExpose(exposeObject); /script 5.操作列按钮封装 (action-button.vue) 对操作列中的按钮单独封装,可以方便我们给操作提供更多丰富的个性化定制配置,根据项目中的需求而定,保证设计的灵活性 template el-popconfirmv-if="confirmProps"v-bind="confirmProps"@confirm="handleConfirm(button,props.scope)" template#reference el-buttonv-bind="buttonProps" {{computedActionName(button,props.scope.row)}} /el-button /template /el-popconfirm el-buttonv-elsev-bind="buttonProps"@click="handleConfirm(button,props.scope)" {{computedActionName(button,props.scope.row)}} /el-button /template scriptsetuplang="ts" import{Action,TColumn}from"./tableColumns"; import{isFunction,isString,isObject}from"@/components/utils/valueTypeCheck"; constprops=withDefaults( defineProps{button:Action;scope:{row:col:$index:number}}(), {} ); constbuttonProps=computed(()={ letcustomeProps:any=props.button.props|| returnObject.assign( { marginRight:"10px", type:"primary", size:"small", }, isFunction(customeProps)?customeProps(props.scope.row):customeProps }); constconfirmProps=computed(()={ constpropsConfirm:any=props.button.confirm; if(propsConfirm===undefined){ returnfalse; } if(!isString(propsConfirm)!isObject(propsConfirm)!isFunction(propsConfirm)){ console.error("confirmProps类型错误"); return } if(isString(propsConfirm)){ return{ title:propsConfirm, } if(isFunction(propsConfirm)){ constres=propsConfirm(props.scope.row); if(isObject(res)){ return } if(isString(res)){ return{ title:res, } } if(isObject(propsConfirm)propsConfirm.title!==undefined){ returnisFunction(propsConfirm.title) ?{ ...propsConfirm, title:propsConfirm.title(props.scope.row), } :propsConfirm; } console.error("confirmProps类型错误"); }); constemits=defineEmits(["click"]); consthandleConfirm=(button,scope:any)={ if(isFunction(button.click)){ emits("click"); } }; constcomputedActionName=(button:Action,row:TColumn)={ return!isFunction(button.text) ?button.text :computed(()=button.text(row)).value?.replace(/\"/g,""); }; /script 6.列表个性化定制封装 (tableSettingDrawer.vue) 个性化定制也是列表常见的需求之一,对于B端业务可能会有不同角色对同一个列表操作的需求,但是相互之间所关注的信息可能不一样。这部分主要是控制对应搜索条件与列表的列展示进行个性化定制。 对于存储设计的话可以用当前页面的路由访问路径作为键来保存,如果同个页面弹窗中还有列表,设计上可以用routePath+id方式来保存,给弹窗中的列表加个id即可。 template el-drawerv-model="dialogVisible"title="个性化定制"direction="rtl"size="50%" el-tabsv-model="currentTab" el-tab-panelabel="定制列"class="setting-content"name="list"@keyup.enter="confirm(originColumns)" el-table:data="originColumns"style="width:100%"table-layout="auto"borderstriperesizable default-expand-all templatev-for="columnincolunms" el-table-columnv-bind="column"#default="{row,col,$index}" spanv-if="column.uiType=='text'"{{row.label}}/span !--输入框-- el-inputv-else-if="column.uiType=='input'"v-model="row[column.field]" :placeholder="`请输入${column.label}`"/el-input !--选择器-- el-selectv-else-if="column.uiType=='select'"v-model="row[column.field]" :placeholder="`请选择${column.label}`" el-optionv-for="optionincolumn.options":key="option.value":label="option.name" :value="option.value"/el-option /el-select !--多选-- el-switchv-else-if="column.uiType=='switch'"v-model="row[column.field]"/el-switch /el-table-column /template /el-table /el-tab-pane el-tab-panelabel="定制查询条件"name="condition"/el-tab-pane /el-tabs template#footer spanclass="dialog-footer" el-button@click="close"取消/el-button el-button@click="$emit('reset',false)"恢复默认设置/el-button el-buttontype="primary"@click="confirm(originColumns)"确定/el-button /span /template /el-drawer /template scriptsetuplang="ts" constcurrentTab=ref("list"); interfaceIProps{ tableRef?:Element; columns:any[]; modelValue?:boolean; } constprops=withDefaults(definePropsIProps(),{ columns:()=[], modelValue:false, }); constdeepCopy=(data)={ returnJSON.parse(JSON.stringify(data)); }; /**采用computed可以实现异步获取配置实时更新**/ constoriginColumns=computed(()=deepCopy(props.columns)); constemit=defineEmits([ "update:modelValue", "update:columns", "refreshColumn", "reset", ]); constconfirm=(tableColumns)={ constcolumns=deepCopy(tableColumns); emit("update:modelValue",false); emit("update:columns",columns); emit("refreshColumn",columns); }; constcolunms=[ {field:"seq",label:"排序",width:60}, {field:"visible",label:"是否展示",uiType:"switch",width:120}, {field:"label",label:"列名",uiType:"text"}, {field:"width",label:"宽度",uiType:"input"}, { field:"align", label:"对齐方式", uiType:"select", options:[ {value:"left",name:"左对齐"}, {value:"right",name:"右对齐"}, {value:"center",name:"居中"}, ], }, { field:"fixed", label:"固定类型", uiType:"select", options:[ {value:"left",name:"左侧"}, {value:"right",name:"右侧"}, {value:"none",name:"不固定"}, ], }, ]; constdialogVisible=ref(false); constopen=()={ dialogVisible.value=true; }; constclose=()={ dialogVisible.value=false; }; defineExpose({ open, close, }); /script 至此,ElTable二次封装相关代码已经结束。希望此中代码能够助各位道友在表格二次封装的设计开发修炼中能有所帮助。 一切大道,皆有因果。喜欢的话,可以动动你的小手点点赞。修行路上愿我们都不必独伴大道,回首望去无故人。 下一篇:[B端开发化神期--这个Vue3动态表单设计是不是你的Crush?](B端开发化神期--这个Vue3动态表单设计是不是你的Crush? - 掘金 (juejin.cn)) 已经完结,[点击此处传送门](B端开发化神期--这个Vue3动态表单设计是不是你的Crush? - 掘金 (juejin.cn)) 阅读原文
| 上一篇:2021-03-01_Hinton独立发布44页论文火爆社区,没有实验:给你们个idea,自己去试吧 | 下一篇:2024-09-02_如何PUA甲方? |
TAG标签: |
18 |
|
我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!
|
|
不达标就退款 高性价比建站 免费网站代备案 1对1原创设计服务 7×24小时售后支持 |
|
|
