最近遇到一个需求:需要实现前端列表页根据后端配置实现自动化生成。期初这个需求是没有头绪的因为就目前接触到的页面来看,用到的列表出了长的比较像意外其实没有完全一样的,基本上没有两个完全没有自定义操作且产品给的原型还是一样的列表。但是遇到需求吗没法的时候就得上。
一般列表页 基本都会含有以下几个结构
一.搜索区(分为条件区域 + 按钮区域)
1)条件区域一般常用的有 输入框,选择框,单选按钮组,多选按钮组,还有联级选择器等、因为要做的是配置所以这里必须尽可能的把每一个条件可能出现的情况考虑到。但是这样的话整个模块就会变得很臃肿。所以第一个版本打算先固定样式和页面风格,同时忽略掉正则这些验证实现简单的版本 大体条件区域的配置可以是这样的
<el-form-item :label="item.label" :key="index" v-for="(item,index) in formList">
<el-input v-if="item.type=='input'||item.type==''" v-model="params[item.param]" :placeholder="item.placeholder"></el-input>
<el-select :disabled="item.disabled" v-if="item.type=='select'" @change="search" clearable filterable v-model="params[item.param]" :placeholder="item.placeholder">
<el-option
v-for="(it,inde) in selectionConfig[item.param] || item.data || []"
:key="inde"
:label="it.value"
:value="it.key">
</el-option>
</el-select>
<el-form-item>
{
"type": "需要使用的条件类型 比如input或者selet等",
"placeholder": "提示语",
"label": "表单的label",
"param": "接口接收此参数的字段名称"
},
这里给一个对象params 接口需要的参数都会挂载到这个对象上 有些条件的选择是需要从接口拿回来数据的 那么拿回来的数据会统一挂载到selectionConfig对象上 相应的selectionConfig的key值最好和查询接口需要的key统一 这样有利于直接取值也比较有规律,要不写接口的人换一下就还得协商一次比较麻烦。当然这里我们也可以写一个转换器来转换拿回来的数据
{
"接口需要的关键值key":"条件对应的关键值key",
"id":"ids",
},
选择的条件大多是从接口拿回来的数据那么配置里面肯定还有一个参数是回去条件的接口地址。这里为了方便处理我们默认一个页面只有一个获取条件的接口,不然接口多了要处理同步异步的话就比较麻烦 我们给这个信息取名为getSelectUrl 而他的转化器配置则为getSelectUrlConfig,
而那些需要实时搜索的下拉我们可以单独进行封装,然后给这个组件定义一个type remote
组件结构
<template>
<el-select
clearable
filterable
remote
:remote-method="getCreaterList"
v-model="val" @change="$emit('input',val)" :placeholder="placeholder?placeholder:''">
<el-option
v-for="(item,index) in selectArr"
:key="index"
:label="item.value"
:value="item.key">
</el-option>
</el-select>
</template>
组件js
<script>
import { api } from "@/api/api";
export default {
props: {
/**
* 支持v-model形式绑定
*/
value: {
type: String,
required: false
},
placeholder:{
type: String,
required: false
},
url:{
type: String,
required: true
}
},
name: "employees",
data() {
return {
selectArr: [],
val:this.value || ""
};
},
computed: {
},
methods: {
/**
* 多太获取列表数据
*@param keyword {String} 搜索关键字
*/
getCreaterList(keyword) {
if (!keyword || keyword.trim() == "") return false;
let self = this;
let config = {
//请求需要的配置信息
};
setTimeout(() => {
//请求处理
self.selectArr = res.data.list || [];
self.$restful.post(config, self, res => {
if (res.code == 200) {
self.selectArr = res.data|| [];
}
});
}, 300);
}
}
};
</script>
那么条件配置也基本成型
{
"type": "remote",
"placeholder": "id",
"label": "",
"param": "id",
//用来转换查询接口和最终参数的配置
"transform":{
"key":"admin_id",
"value":"cn_name",
"secondValue":"admin_name"
}
"url":"动态获取的url"
},
处理当前列表是否默认查询
searchConfig.immediatelySearch = true;//true,默认进入页面就查询,false默认不查询
到这里基本需要配置也完成了
list 完整 代码
<!-- 查询列表模板 -->
<template>
<el-container class="search__list_for_table">
<el-main v-loading="pageLoading ||loading">
<!-- 条件和搜索结果 若无直接 v-if="0" 或者直接删除-->
<el-row class="select_conditions mgt10">
<!--搜索条件部分 若无直接 v-if="0" 或者直接删除-->
<el-row class="text-left el-collapse-item active" :class="!isShowMoreSearch?'active':''">
<el-form :inline="true" :label-position="labelPosition" :model="params">
<template >
<el-form-item :label="item.label" :key="index" v-for="(item,index) in formList">
<!-- 挂载在 searchConfig.model对象上-->
<el-input v-if="item.type=='input'||item.type==''" v-model="params[item.param]" :placeholder="item.placeholder"></el-input>
<Employees v-if="item.type=='employees'" v-model="params[item.param]" :placeholder="item.placeholder"></Employees>
<RemoteCommon :url="item.url" v-if="item.type=='remote'" :transform="item.transform" v-model="params[item.param]" :placeholder="item.placeholder"></RemoteCommon>
<el-select :disabled="item.disabled" v-if="item.type=='select'" @change="search" clearable filterable v-model="params[item.param]" :placeholder="item.placeholder">
<el-option
v-for="(it,inde) in selectionConfig[item.param] || item.data || []"
:key="inde"
:label="it.value"
:value="it.key">
</el-option>
</el-select>
<el-cascader
v-if="item.type=='cascader'"
v-model="params[item.param]"
:options="selectionConfig[item.param] || item.data || []"
:placeholder="item.placeholder"
:change-on-select="item.changeOnSelect?true:false"
:props="{
value:'key',
label:'value'
}"
></el-cascader>
<el-radio-group @change="search" v-if="item.type=='radio_group'" v-model="params[item.param]" size="small">
<template>
<el-radio-button :label="it.key" :key="ind" v-for="(it,ind) in selectionConfig[item.param] || item.data">{{it.value}}</el-radio-button>
</template>
</el-radio-group>
<el-date-picker
v-if="item.type=='date_picker'"
v-model="params[item.param]"
size="mini"
style="width:280px"
@change="search"
type="daterange"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
:picker-options="pickerOptions"
:range-separator="$t('message.time.to')"
:start-placeholder="$t('message.time.start_time')"
:end-placeholder="$t('message.time.end_time')"
align="right">
</el-date-picker>
</el-form-item>
</template>
<el-form-item label="">
<el-button type="primary" @click="search">{{$t("message.button.search")}}</el-button>
<el-button type="" @click="reset">{{$t("message.button.reset")}}</el-button>
<!--导出Excel-->
<el-button size="mini" type="" v-if="showMoreSearch" @click="isShowMoreSearch = !isShowMoreSearch">{{$t("message.button.more")}}
<i class="el-icon-arrow-up pd0" style="font-size:12px;" :class="!isShowMoreSearch?'spreed-up':'spreed-down'"></i>
</el-button>
<el-button type="" v-if="searchConfig.exportUrl" @click="exportFn">
{{$t("message.closeAccount_button.export")}}{{searchConfig.exportUrl}}
</el-button>
</el-form-item>
<el-form-item label="" v-if="searchConfig.operations">
<component @search="search" @setLoading="setLoading" :params ="params" :multipleSelection="multipleSelection" :is="components[searchConfig.operations]"></component>
</el-form-item>
<!-- el-collapse-transition 若需要展开和收起不可以省略或者删除 -->
<el-collapse-transition>
<!-- 纯条件部分 默认展开 若无直接 v-if="0" 或者直接删除-->
<div v-show="isShowMoreSearch" class="more-options ">
<!-- 此处为条件根据实际项目情况填写内容 -->
<template >
<el-form-item :label="item.label" :key="index" v-for="(item,index) in formListMore">
<!-- 挂载在 searchConfig.model对象上-->
<el-input v-if="item.type=='input'||item.type==''" v-model="params[item.param]" :placeholder="item.placeholder"></el-input>
<el-select :disabled="item.disabled" v-if="item.type=='select'" @change="search" clearable filterable v-model="params[item.param]" :placeholder="item.placeholder">
<el-option
v-for="(it,inde) in selectionConfig[item.param] || item.data || []"
:key="inde"
:label="it.value"
:value="it.key">
</el-option>
</el-select>
<Employees v-if="item.type=='employees'" v-model="params[item.param]" :placeholder="item.placeholder"></Employees>
<RemoteCommon :url="item.url" v-if="item.type=='remote'" :transform="item.transform" v-model="params[item.param]" :placeholder="item.placeholder"></RemoteCommon>
<el-cascader
v-if="item.type=='cascader'"
v-model="params[item.param]"
:options="selectionConfig[item.param] || item.data || []"
:placeholder="item.placeholder"
change-on-select
:props="{
value:'key',
label:'value'
}"
></el-cascader>
<el-radio-group @change="search" v-if="item.type=='radio_group'" v-model="params[item.param]" size="small">
<template>
<el-radio-button :label="it.key" :key="ind" v-for="(it,ind) in selectionConfig[item.param] || item.data">{{it.value}}</el-radio-button>
</template>
</el-radio-group>
<el-date-picker
v-if="item.type=='date_picker'"
v-model="params[item.param]"
size="mini"
style="width:280px"
@change="search"
type="daterange"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
:picker-options="pickerOptions"
:range-separator="$t('message.time.to')"
:start-placeholder="$t('message.time.start_time')"
:end-placeholder="$t('message.time.end_time')"
align="right">
</el-date-picker>
</el-form-item>
</template>
</div>
</el-collapse-transition>
</el-form>
</el-row>
<!-- 需要展示的结果页面 不可以删除 没有结果 做什么搜索 -->
<el-row class=" result__tabel">
<!-- 表格展示区 如果是其他的内容不是表格就把下面换了吧 -->
<!-- v-loading='loading' -->
<el-table
:data="listData"
style="width: 100%"
:border="tableConfig.border?true:false"
size="mini"
:empty-text="$t('message.label.no_data')"
:max-height="tableConfig.tableHeight?tableConfig.tableHeight:maxHeight"
@selection-change="handleSelectionChange"
>
<el-table-column v-if="tableConfig.selection" type="selection" width="55"></el-table-column>
<el-table-column
v-for="(item,index) in tableConfig.tableHeaderArr"
:width="item.width || ''"
:prop="item.key"
:key="index"
:label="item.value">
<template slot-scope="scope">
<component @setLoading="setLoading" v-if="item.components" :data="scope.row" @search="search" :is="components[item.components]"></component>
<div v-else >
<el-popover v-if="item.showOverflowTooltip && scope.row[item.key]" trigger="hover" placement="top" :width="item.width || 300">
<p> {{ scope.row[item.key] }}</p>
<div slot="reference" class="name-wrapper" style="cursor:pointer;">
<el-tag size="medium"
:style="`max-width: ${item.width-10 || 280}px;overflow: hidden;text-overflow: ellipsis; white-space: nowrap;`"> <span> {{ scope.row[item.key] }}</span></el-tag>
</div>
</el-popover>
<div v-else v-html="scope.row[item.key]"></div>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('message.label.operations')" v-if="tableConfig.operations" :width="tableConfig.operationsWidth||'200px'">
<template slot-scope="scope">
<component @setLoading="setLoading" :data="scope.row" @search="search" :is="components[tableConfig.operations]"></component>
</template>
</el-table-column>
</el-table>
<!-- 页码处理模块 若无页码处理则直接省略 或者v-if="0" -->
<el-row class="pdt40" v-if="tableConfig.isNeedPage!=false">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="page"
:page-sizes="[20, 40, 60, 100]"
:page-size="page_size"
:total='page_info.total'
layout="total, sizes, prev, pager, next, jumper"
:page-count='Math.ceil(page_info.total/page_size)'
style="text-align: right;"
class="pt20"
></el-pagination>
</el-row>
</el-row>
</el-row>
<DownloadExcel ref="downloadExcel" :url="searchConfig.exportUrl"></DownloadExcel>
</el-main>
</el-container>
</template>
<script>
// 混合查询的方法
import { listSearchMixin } from "@/script/mixin/mixin";
import { api } from "@/api/api";
import { components } from "@/config/components";
import store from "@/store/store";
import DownloadExcel from "@/components/common/upload/downloadExcel";
import RemoteCommon from "@/components/common/remoteSearch/common";
import Employees from "@/components/common/remoteSearch/employees";
export default {
// 组件名称
name: "componentName",
// 混合模式, 复用组件的内容
mixins: [listSearchMixin],
// 父子通信
props: {},
components: { DownloadExcel,Employees,RemoteCommon},
watch: {},
computed: {
formList() {
return this.searchConfig.formCom;
},
formListMore() {
return this.searchConfig.formComMore;
},
// 是否需要收起展开搜索条件的 条件
showMoreSearch() {
return this.searchConfig.isShowMoreSearch ? true : false;
},
// 适配小屏幕显示器,列表页尽量在一个页面展示完
maxHeight(){
let height = document.body.clientHeight
return this.isShowMoreSearch ?parseInt(height*(height <= 800 ?0.45:0.6)):parseInt(height*(height <= 800 ?0.6:0.70))
}
},
beforeOpen(resolve, reject) {
// 组件实例化之前需要进行的操作-比如获取配置 这里以获取banner配置为例
//store.dispatch("getBannerConfig").then(resolve); // 获取apponly的配置
resolve();
},
// 数据绑定
data() {
return {
//组件库
components: components,
// 页面缓冲
pageLoading: false,
// 查询条件
selectionConfig: {},
// 搜索配置
/*
* */
// 查询地址
searchUrl: "",
// 配置地址
configUrl: "",
// 配置
searchConfig: {
export_url: api.closeAccDownloadExcel,
// 当前操作部分组件名称
operations: "starListOperations",
// 是否显示多选框
selection: true,
// 接收参数的名称
model: "params",
// 搜索条件
formCom: []
},
tableConfig: {
// 最后操作组件名称
operations: "starListOperations",
// 当前表格高度
tableHeight: 600,
// 当前表格头部
tableHeaderArr: [
{
value: "表格头显示的内容",
key: "表格头对应的后端返回字段",
width: "表格列的宽度",
components: "当前列对应的组件名称"
}
]
},
/*数据按照最简单模型, todo 下拉走接口。radio默认值处理*/
// 查询参数 具体自定义内容
params: {},
paramsArr: [],
//表格缓冲动画显示条件
loading: false,
// 展示更多控制区
isShowMoreSearch: false,
// 表格区多选后的结果存储数组
multipleSelection: [],
// 页码
page: 1,
// 一页显示多少条数据
page_size: 20,
// 接受后台返回的页数据
page_info: {},
// label对齐方式
labelPosition: "left",
// 连选日期快捷变量
pickerOptions: {
shortcuts: [
{
text: this.$t("message.time.in_seven"),
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit("pick", [start, end]);
}
},
{
text: this.$t("message.time.in_fifteen"),
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 15);
picker.$emit("pick", [start, end]);
}
},
{
text: this.$t("message.time.in_month"),
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit("pick", [start, end]);
}
}
]
}
};
},
// 方法 查询方法默认为 search方法 无需再次定义 直接使用即可
methods: {
/**
* @desc 设置页面是否为缓冲状态
* @param bool {Boolean}缓冲状态值
*/
setLoading(bool){
this.pageLoading = bool
},
// 重置查询条件 在这里重置查询条件吧
reset() {
this.params = Object.assign({},this.com.defaultParam ||{});
// 页码
this.page = 1;
// 一页显示多少条数据
this.page_size = 20;
},
/*
* 获取参数,必须在组件定义 使用混合查询不可省略否则无法获取查询参数
*/
getParams() {
let self = this;
// 若有查询条件需要判断的可以在此处判断
let data = {};
data =
self.tableConfig.isNeedPage == false
? {
...self.params
}
: {
...self.params,
...{
page_size: self.page_size,
page: self.page
}
};
return {
url: api.baseUrl + self.searchUrl + api.token,
contentType:self.searchConfig.searchContentType || "form",
data: data
};
},
// 导出Excel
exportFn() {
let self = this;
let arr = [];
for (let key in self.params) {
let obj = {
key: key,
value: self.params[key]
};
arr.push(obj);
}
this.$refs["downloadExcel"].init(arr);
},
/**
* 多选框操作
*@param val {Array} 当前选中的内容
*/
handleSelectionChange(val) {
this.multipleSelection = val;
},
// 刷新-通用方法
refresh() {
this.search();
},
init(config) {
this.config = config;
}
},
beforeCreate() {},
created() {
let self = this;
self.pageLoading = true;
new Promise((resolve, reject) => {
let idx = self.com.configUrl.indexOf(".json");
let url = "";
if (idx > -1) {
let lang = localStorage.getItem("lang") || "zh-CN"
url = "/static/AUTOListConfig/"+
self.com.configUrl.split(".json")[0] +
lang +
".json";
} else {
url = api.getConfigOfList;
}
self.$restful.get(
{
url: url,
param: {
filename: idx == -1 ? self.com.configUrl : ""
}
},
self,
res => {
if (res.code == 200) {
self.tableConfig = res.data.tableConfig;
self.searchConfig = res.data.searchConfig;
self.searchUrl = res.data.searchConfig.searchUrl;
let defaultParam = {};
if(res.data.searchConfig.default){
defaultParam = res.data.searchConfig.default
}else if(self.com.defaultParam){
defaultParam = JSON.parse(JSON.stringify(self.com.defaultParam))
}
if(defaultParam){
self.params = Object.assign(defaultParam)
}
if (res.data.searchConfig.getSelectUrl) {
self.$restful.get(
{ url:api.baseUrl + res.data.searchConfig.getSelectUrl + api.token, param: res.data.searchConfig.getSelectParams||{} },
self,
resp => {
if (resp.code == 200) {
if(res.data.searchConfig.getSelectConfig){
let o = res.data.searchConfig.getSelectConfig
for(let key in o){
self.selectionConfig[key] = resp.data[o[key]]
}
}else{
self.selectionConfig = resp.data;
}
resolve(res.data.searchConfig.immediatelySearch?true:false);
} else {
reject();
}
},
error => {
reject();
}
);
} else {
resolve(res.data.searchConfig.immediatelySearch?true:false);
}
} else {
reject();
}
},
error => {
reject();
}
);
})
.then(function(res) {
self.pageLoading = false;
res && self.search()
})
.catch(function(rej) {
self.pageLoading = true;
});
},
mounted() {
let self = this;
}
};
</script>
<style lang="less" rel="stylesheet/less">
</style>
配置模拟返回json文件
{
"code": 200,
"data": {
"tableConfig": {
"addConfigUrl":"",
"addSaveUrl":"",
"editSaveUrl":"",
"detailUrl":"",
"editConfigUrl":"",
"operations": "labelListHandel",
"isNeedPage":true,
"border":false,
"tableHeaderArr": [{
"value": "在线邮寄方式",
"key": "shipping_service",
"width":""
},
{
"value": "仓库",
"key": "warehouse_name",
"components": "",
"width":"150"
},
{
"value": "状态",
"key": "is_use",
"components":"changeStatus",
"width":"200"
},
{
"value": "更新人",
"key": "oa_create_user_name",
"width":"150"
},
{
"value": "更新时间",
"key": "edit_date",
"width":"300"
}
]
},
"searchConfig": {
"isShowMoreSearch":false,
"immediatelySearch":true,
"exportUrl": "",
"searchUrl":"/shipment/shippingList",
"getSelectUrl":"/shipment/getSearchFields",
"getSelectConfig":{
"shipping_method_id":"shipping_method_list",
"warehouse_id":"warehouse_list"
},
"getSelectParams":{
"shipping_method_list":1,
"warehouse_list":1,
"country_list":1,
"tag_list":1,
"tag_type_list":1,
"country_type_list":1
},
"default":{
"status":"1"
},
"operations": "openLabelSearch",
"operationsWidth":"300px",
"model": "params",
"formCom": [{
"type": "select",
"placeholder": "在线邮寄方式",
"label": "",
"param": "shipping_method_id"
},
{
"type": "select",
"placeholder": "仓库",
"label": "",
"param": "warehouse_id",
"data": []
},
{
"type": "select",
"placeholder": "是否启用",
"label": "",
"param": "status",
"data": [{
"key": "1",
"value": "是"
},
{
"key": "0",
"value": "否"
}
]
},
{
"type": "employees",
"placeholder": "更新人",
"label": "",
"param": "admin_id"
}
],
"formComMore":[
]
}
}
}
image.png
默认的我们会把所有的组件挂载到这个对象上
image.png
以下是这个组件的示例 components.js
/**
*
* @desc 列表操作部分使用import 引入 搜索操作使用 require 动态引入
*/
const listButton = r => require(['@/components/pages/hotManSystem/listButton'], r)
// 新增标签
const openLabelSearch = r => require(['@/components/pages/OADelivery/openLabelSearch'], r)
// 新增快递方式
const addMethodDelivery = r => require(['@/components/pages/OADelivery/addMethodDelivery'], r)
//列表内的组件一定要使用import的方式引入 如果动态引入第一次查询的时候将不会渲染组件
// 外部链接
import outLink from "@/components/common/link/outLink";
// 标签操作
import labelListHandel from "@/components/pages/OADelivery/labelListHandel";
//
export const components = {
// 公共的的组件
outLink: outLink,
"openLabelSearch": openLabelSearch,
// 查看标签
"labelListHandel": labelListHandel,
//快递方式搜索新增
"addMethodDelivery": addMethodDelivery,
//列表操作组件
"methodDeliveryOpptions": methodDeliveryOpptions,
}
在所有的自定义组件上 我们将会挂载列表页可能的数据到组件上 方便在调用到的子组件内使用 并定义回调方法
image.png image.png
混合查询的通用方法searchMixin.js
/*
* 列表搜索的 mixin
*/
import {
resCode
} from '@/config/consts'
import {
getExportFormComItem
} from '@/config/tools'
export const listSearch = {
data () {
return {
// 加载中
loading: false,
// 页面的列表数据
listData: []
}
},
methods: {
/*
* 处理页面序号的变化
*/
indexMethod (index) {
if (this.page === 1 || this.page === 0) return index + 1
return ((this.page - 1) * this.page_size) + index + 1
},
/*
* 搜索函数
*/
search () {
let self = this
let params = self.getParams()
if (!params) return false
self.loading = true
let config = {
url:params.url,
loading:params.loading || "loading",
contentType:params.contentType || "form",
param:params.data || {}
}
// 若为单选则更新
self.$restful.sendMyData(config,self, (res)=> {
if(res.code === 200)
self.listData = res.data.rows || []
if (res.data.total || res.data.total ===0) {self.page_info.total = Number(res.data.total)}
})
// self.elForm 为导出组件的引用,如果有用到,需要在组件的mounted中定义如: self.elForm = self.$refs['elForm']
// 调用导出插件的刷新函数
// getExportFormComItem 的参数为【页面搜索的条件】,为 {} 类型
self.elForm && self.elForm.refresh(getExportFormComItem(params.data))
},
/*
* 改变页面显示的条数
*/
handleSizeChange (page_size) {
this.page_size = page_size
this.search()
},
/*
* 改变页码
*/
handleCurrentChange (page) {
this.page = page
this.search()
},
/*
* 获取参数,这个方法必须在组件中去覆盖,不然应该提示错误
* 可以理解为后台的抽象函数
*/
getParams () {
let self = this
self.$message.error('请在组件的methods中定义获取参数的方法 [ getParams ]')
return false
},
}
}
合并混合查询方法
mixin.js
import { listSearch } from './searchMixin.js'
export const listSearchMixin = {
...listSearch
}
以上就是简单的自动化配置列表的全部代码了 后面的可以根据具体的业务做条件和其他业务模块的扩展
网友评论