安装和引入
安装
// npm
npm install common-pro-table
// yarn
yarn add common-pro-table
引入
该组件依赖 element-plus,需要自行引入
// 引入element-plus import ElementPlus from 'element-plus' import 'element-plus/lib/theme-chalk/index.css' // 引入中文语言包 import 'dayjs/locale/zh-cn' import locale from 'element-plus/lib/locale/lang/zh-cn' app.use(ElementPlus, { locale })
// 引入vue-pro-table
import commonProTable from 'common-pro-table'
app.use(commonProTable)
表格配置
-
border 表格是否有边框。布尔值,默认为 false
-
columns 属性的配置,是一个数组
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
label | 对应 el-table-column 的 label | string | - | - |
type | 对应 el-table-column 的 type | string | selection/index/expand | - |
prop | 对应 el-table-column 的 prop | string | - | - |
width | 对应 el-table-column 的 width | string,number | - | - |
minWidth | 对应 el-table-column 的 min-width | string,number | - | - |
align | 对应 el-table-column 的 align | string | left/center/right | left |
fixed | 对应 el-table-column 的 fixed | string, boolean | true, left, right | - |
sortable | 对应 el-table-column 的 sortable | boolean | false/true | false |
filters | 对应 el-table-column 的 filters | Array[{ text, value }] | - | - |
tdSlot | 单元格要自定义内容时,可以通过此属性配置一个插槽名称,并且是作用域插槽,可以接收 scope 数据 | string | - | - |
labelSlot | 表头要自定义内容时,可以通过此属性配置一个插槽名称,并且是作用域插槽,可以接收 scope 数据 | string | - | - |
-
row-key 属性配置
对应 el-table 的 row-key,默认值是'id'
搜索配置
-
search 属性的配置,是一个对象
如果不想显示搜索表单,可以不配置 search 或者 search 设置为 false
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
labelWidth | label 文字长度 | string | - | - |
inputWidth | 表单项长度 | string | - | - |
fields | 表单字段列表,包含 text,select,radio,checkbox,datetime 等类型,所有字段类型配置见下表 | Array[{字段类型}] | - | - |
- fields 列表的字段类型配置
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
type | 字段类型 | string | text,textarea,select,radio,radio-button,checkbox,checkbox-button,number,date,daterange,datetime,datetimerange | text |
label | label 文本 | string | - | - |
name | 搜索时的提交的参数名称 | string | - | - |
style | 额外的样式 | object | - | - |
defaultValue | 默认值 | - | - | |
options | 当 type 是 select,radio,radio-button,checkbox,checkbox-button 时的枚举选项 | Array[{name, value}] | - | - |
filterable | 当 type 是 select 时,下拉框是否支持模糊搜索 | boolean | true, false | false |
multiple | 当 type 是 select 时,下拉框是否支持多选 | boolean | true, false | false |
transform | 搜索前对表单数据进行转换,比如表单数据是数组,但是搜索的时候需要传递字符串。它是一个函数,默认参数是字段的 value,需要返回转换后的结果 | function(value) | - | - |
trueNames | 当 type 是 daterange,datetimerange 时,开始时间和结束时间是在一个数组里面,但是搜索时可能需要两个字段,这时就需要把开始时间和结束时间分别赋值给两个字段,这两个字段的名称就是通过 trueNames 配置,它是一个数组,例如:trueNames: ['startTime', 'endTime'] | Array[string] | ||
min | 当 type 是 number 时的最小值 | number | - | - |
max | 当 type 是 number 时的最大值 | number | - | - |
分页配置
-
pagination 属性的配置,是一个对象
如果不想显示分页,将 pagination 设置为 false
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
layout | 组件布局 | string | total, sizes, prev, pager, next, jumper | total, sizes, prev, pager, next, jumper |
pageSize | 每页显示条目个数 | number | - | 10 |
pageSizes | 每页显示个数选择器的选项设置 | Array[number] | - | [10, 20, 30, 40, 50, 100] |
标题栏配置
表格上方有一个标题栏,标题栏左侧显示一个标题,右侧是一个可自定义的工具栏
-
hide-title-bar
是否隐藏标题栏,布尔值
-
title
表格标题
用法
<template>
<div class="page-box">
<!-- 搜索选项 -->
<div class="search" v-if="search">
<div class="head" v-if="search.name">
<span v-if="search.icon" class="icon"></span>
{{ search.name }}
</div>
<el-form
:model="searchModel"
:inline="true"
:label-position="search.labelPosition"
:label-width="search.labelWidth"
ref="searchForm"
:style="search.style"
>
<el-form-item
v-for="item in search.fields"
:key="item.name"
:label="item.label"
:prop="item.name"
>
<slot v-if="item.type === 'custom'" :name="item.slot" />
<el-select
v-else-if="item.type === 'select'"
v-model="searchModel[item.name]"
:filterable="!!item.filterable"
:multiple="!!item.multiple"
clearable
:placeholder="`${item.placeholder}`"
:style="{ width: search.inputWidth, ...item.style }"
>
<el-option
v-for="option of item.options"
:key="option.code"
:label="option.name"
:value="option.code"
>
</el-option>
</el-select>
<el-input
v-else-if="item.type === 'tel'"
v-model="searchModel[item.name]"
:placeholder="`${item.placeholder}`"
:style="{ width: search.inputWidth, ...item.style }"
>
<template #prepend v-if="item.select">
<el-select
v-model="searchModel[item.select.name]"
:placeholder="`${item.select.placeholder}`"
:style="{ width: item.select.width, ...item.select.style }"
>
<template #prefix>
<span> + </span>
</template>
<el-option
v-for="option in item.select.options"
:key="option.code"
:label="option.name"
:value="option.code"
>
</el-option>
</el-select>
</template>
</el-input>
<el-radio-group
v-model="searchModel[item.name]"
v-else-if="item.type === 'radio'"
:style="{ width: search.inputWidth, ...item.style }"
>
<el-radio
v-for="option of item.options"
:key="option.code"
:label="option.code"
>
{{ option.name }}
</el-radio>
</el-radio-group>
<el-radio-group
v-model="searchModel[item.name]"
v-else-if="item.type === 'radio-button'"
:style="{ width: search.inputWidth, ...item.style }"
>
<el-radio-button
v-for="option of item.options"
:key="option.code"
:label="option.code"
>
{{ option.name }}
</el-radio-button>
</el-radio-group>
<el-checkbox-group
v-model="searchModel[item.name]"
v-else-if="item.type === 'checkbox'"
:style="{ width: search.inputWidth, ...item.style }"
>
<el-checkbox
v-for="option of item.options"
:key="option.code"
:label="option.code"
>
{{ option.name }}
</el-checkbox>
</el-checkbox-group>
<el-checkbox-group
v-model="searchModel[item.name]"
v-else-if="item.type === 'checkbox-button'"
:style="{ width: search.inputWidth, ...item.style }"
>
<el-checkbox-button
v-for="option of item.options"
:key="option.code"
:label="option.code"
>
{{ option.name }}
</el-checkbox-button>
</el-checkbox-group>
<el-date-picker
v-else-if="item.type === 'week'"
v-model="searchModel[item.name]"
type="week"
clearable
:placeholder="`${item.placeholder}`"
:style="{ width: search.inputWidth, ...item.style }"
>
</el-date-picker>
<el-date-picker
v-else-if="item.type === 'month'"
v-model="searchModel[item.name]"
type="month"
clearable
:placeholder="`${item.placeholder}`"
:style="{ width: search.inputWidth, ...item.style }"
>
</el-date-picker>
<el-date-picker
v-else-if="item.type === 'year'"
v-model="searchModel[item.name]"
type="year"
clearable
:placeholder="`${item.placeholder}`"
:style="{ width: search.inputWidth, ...item.style }"
>
</el-date-picker>
<el-date-picker
v-else-if="item.type === 'date'"
v-model="searchModel[item.name]"
type="date"
format="YYYY-MM-DD"
clearable
@change="handleDateChange($event, item, 'YYYY-MM-DD')"
:placeholder="`${item.placeholder}`"
:style="{ width: search.inputWidth, ...item.style }"
>
</el-date-picker>
<el-date-picker
v-else-if="item.type === 'datetime'"
v-model="searchModel[item.name]"
type="datetime"
clearable
@change="handleDateChange($event, item, 'YYYY-MM-DD HH:mm:ss')"
format="YYYY-MM-DD HH:mm:ss"
:placeholder="`${item.placeholder}`"
:style="{ width: search.inputWidth, ...item.style }"
>
</el-date-picker>
<el-date-picker
v-else-if="item.type === 'daterange'"
v-model="searchModel[item.name]"
type="daterange"
format="YYYY-MM-DD"
:range-separator="item.rangeSeparator"
:start-placeholder="`${item.startPlaceholder}`"
:end-placeholder="`${item.endPlaceholder}`"
clearable
@change="handleRangeChange($event, item, 'YYYY-MM-DD')"
:style="{ width: search.inputWidth, ...item.style }"
>
</el-date-picker>
<el-date-picker
v-else-if="item.type === 'datetimerange'"
v-model="searchModel[item.name]"
type="datetimerange"
format="YYYY-MM-DD HH:mm:ss"
:range-separator="item.rangeSeparator"
:start-placeholder="`${item.startPlaceholder}`"
:end-placeholder="`${item.endPlaceholder}`"
clearable
@change="handleRangeChange($event, item, 'YYYY-MM-DD HH:mm:ss')"
:style="{ width: search.inputWidth, ...item.style }"
>
</el-date-picker>
<el-input-number
v-else-if="item.type === 'number'"
v-model="searchModel[item.name]"
:placeholder="`${item.placeholder}`"
controls-position="right"
:min="item.min"
:max="item.max"
:style="{ width: search.inputWidth, ...item.style }"
/>
<el-input
v-else-if="item.type === 'textarea'"
type="textarea"
clearable
v-model="searchModel[item.name]"
:placeholder="`${item.placeholder}`"
:style="{ width: search.inputWidth, ...item.style }"
>
</el-input>
<el-input
v-else
v-model="searchModel[item.name]"
clearable
:placeholder="`${item.placeholder}`"
:style="{ width: search.inputWidth, ...item.style }"
>
</el-input>
</el-form-item>
<el-form-item class="search-btn">
<sinosoft-common-button
v-for="item in search.btns"
:key="item.name"
:btnCode="item.btnCode"
:plain="item.plain"
:primary="item.primary"
:default="item.default"
:info="item.info"
:circle="item.circle"
:Width="item.Width"
:height="item.height"
:color="item.color"
:bgColor="item.bgColor"
:borderColor="item.borderColor"
:round="item.round"
:loading="item.loading"
:loadingColor="item.loadingColor"
:borderStyle="item.borderStyle"
:icon="item.icon"
:size="item.size"
:style="{ ...item.style }"
@handleClick="item.fn"
>
{{ item.name }}
</sinosoft-common-button>
<!-- <el-button
v-for="item in search.btns"
:key="item.name"
@click="item.fn"
>
{{ item.name }}
</el-button> -->
<!-- <el-button @click="handleSearch" icon="el-icon-search" >
查询
</el-button> -->
</el-form-item>
</el-form>
</div>
<!-- table表格栏 -->
<div class="table" v-for="(tableItem, idx) in tableConfig" :key="idx">
<!-- title 和 工具栏 -->
<div class="head" v-if="!tableItem.hideTitleBar">
<slot name="title">
<span class="title">{{ tableItem.title }}</span>
</slot>
<div
class="toolbar"
v-if="tableItem.toolbar && tableItem.toolbar.length > 0"
>
<slot :name="tableItem.toolbar"></slot>
<sinosoft-common-button
v-for="item in tableItem.toolbar"
:key="item.name"
:btnCode="item.btnCode"
:plain="item.plain"
:primary="item.primary"
:default="item.default"
:info="item.info"
:circle="item.circle"
:Width="item.Width"
:height="item.height"
:color="item.color"
:bgColor="item.bgColor"
:borderColor="item.borderColor"
:round="item.round"
:loading="item.loading"
:loadingColor="item.loadingColor"
:borderStyle="item.borderStyle"
:icon="item.icon"
:size="item.size"
:style="{ ...item.style }"
@handleClick="item.fn"
>
{{ item.name }}
</sinosoft-common-button>
<!-- <el-button
v-for="item in tableItem.toolbar"
:key="item.name"
@click="item.fn"
>
{{ item.name }}
</el-button> -->
</div>
</div>
<el-table
v-loading="tableItem.loading"
:ref="tableItem.ref"
:data="tableItem.tableData"
:tree-props="tableItem.tree.treeProps"
:lazy="tableItem.tree.lazy"
:load="tableItem.tree.load"
tooltip-effect="dark"
:header-cell-style="{}"
:cell-style="{}"
stripe
:border="tableItem.border"
:max-height="tableItem.maxHeight"
@select="tableItem.select"
@selection-change="tableItem.handleSelectionChange"
@sort-change="tableItem.handleSortChange"
>
<el-table-column
v-for="columnItem in tableItem.columns"
:key="columnItem.label"
:filter-method="columnItem.filters && filterHandler"
:show-overflow-tooltip="!columnItem.wrap"
:sort-method="columnItem.sortMethod"
v-bind="columnItem"
>
<template #header="scope" v-if="!!columnItem.labelSlot">
<slot :name="columnItem.labelSlot" v-bind="scope"></slot>
</template>
<template #default="scope" v-if="!!columnItem.tdSlot">
<slot :name="columnItem.tdSlot" v-bind="scope"></slot>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-if="tableItem.paginationConfig.show && tableItem.total > 0"
class="pagination"
:background="tableItem.paginationConfig.background"
:style="tableItem.paginationConfig.style"
@size-change="tableItem.handleSizeChange"
v-model:currentPage="tableItem.pageNum"
@current-change="tableItem.handleCurrentChange"
:page-sizes="tableItem.paginationConfig.pageSizes"
v-model:pageSize="tableItem.pageSize"
:layout="tableItem.paginationConfig.layout"
:total="tableItem.total"
>
</el-pagination>
</div>
</div>
</template>
<script lang="ts">
import {
defineComponent,
reactive,
toRefs,
onBeforeMount,
// ref,
// nextTick,
} from 'vue'
// import sinosoftCommonButton from 'sinosoft-common-button'
// import '../../node_modules/sinosoft-common-button/style.css'
const formatDate = (date: any, format: any) => {
var obj = {
'M+': date.getMonth() + 1,
'D+': date.getDate(),
'H+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds(),
'q+': Math.floor((date.getMonth() + 3) / 3),
'S+': date.getMilliseconds(),
}
if (/(y+)/i.test(format)) {
format = format.replace(
RegExp.$1,
(date.getFullYear() + '').substr(4 - RegExp.$1.length)
)
}
for (var k in obj) {
if (new RegExp('(' + k + ')').test(format)) {
format = format.replace(
RegExp.$1,
RegExp.$1.length == 1
? obj[k]
: ('00' + obj[k]).substr(('' + obj[k]).length)
)
}
}
return format
}
const getSearchModel = (search: any) => {
const searchModel = {}
if (search && search?.fields) {
search.fields.forEach((item: any) => {
switch (item.type) {
case 'checkbox':
case 'checkbox-button':
searchModel[item.name] = []
break
default:
break
}
if (item.defaultValue !== undefined) {
searchModel[item.name] = item.defaultValue
if (item.type === 'tel' && item.select) {
searchModel[item.select.name] = item.select.defaultValue
}
// 日期范围和时间范围真实变量默认值
if (
(item.type === 'daterange' || item.type === 'datetimerange') &&
!!item.trueNames &&
Array.isArray(item.defaultValue)
) {
item.defaultValue.forEach((val: any, idx: any) => {
searchModel[item.trueNames[idx]] = val
})
}
}
})
}
return searchModel
}
export default defineComponent({
props: {
// 搜索配置
search: {
type: Object,
default: {},
},
// 表格配置
tableConfig: {
type: [Object],
default: [],
},
},
components: {
// button 组件
// sinosoftCommonButton,
},
setup(props: any, { emit }: any) {
// 优化字段
const optimizeFields = (search: any) => {
const searchModel = JSON.parse(JSON.stringify(state.searchModel))
if (search && search.fields) {
search.fields.forEach((item: any) => {
if (!searchModel.hasOwnProperty(item.name)) {
return
}
if (!!item.transform) {
searchModel[item.name] = item.transform(searchModel[item.name])
}
if (
(item.type === 'daterange' || item.type === 'datetimerange') &&
!!item.trueNames
) {
delete searchModel[item.name]
}
})
}
return searchModel
}
onBeforeMount(() => {
state.searchModel = optimizeFields(props.search)
emit('getTableData', { searchModel: state.searchModel, pageNum: 1 })
})
const state = reactive({
searchModel: getSearchModel(props.search),
// paginationConfig: {},
// 搜索
handleSearch() {
emit('getTableData', { searchModel: state.searchModel, pageNum: 1 })
},
// 重置
handleReset() {
if (JSON.stringify(state.searchModel) === '{}') {
return
}
state.searchModel = getSearchModel(props.search)
console.log(state.searchModel)
emit('getTableData', { searchModel: state.searchModel, pageNum: 1 })
},
// 刷新
refresh() {
emit('getTableData', { searchModel: state.searchModel, pageNum: 1 })
},
// 过滤
filterHandler(value: any, row: any, column: any) {
const property = column['property']
return row[property] === value
},
// 日期范围
handleDateChange(date: any, item: any, format: any) {
state.searchModel[item.name] = !!date ? formatDate(date, format) : ''
},
handleRangeChange(date: any, item: any, format: any) {
const arr = !!date && date.map((d: any) => formatDate(d, format))
state.searchModel[item.name] = !!arr ? arr : []
if (!item.trueNames) {
return
}
if (!!arr) {
arr.forEach((val: any, idx: any) => {
state.searchModel[item.trueNames[idx]] = val
})
} else {
item.trueNames.forEach((key: any) => {
delete state.searchModel[key]
})
}
},
})
// props.table.forEach((item: any) => {
// if (typeof item.paginationConfig === "object") {
// const { layout, pageSizes, style } = item.paginationConfig;
// state.paginationConfig = {
// show: true,
// layout: layout || "total, sizes, prev, pager, next, jumper",
// pageSizes: pageSizes || [10, 20, 30, 40, 50, 100],
// style: style || {},
// };
// }
// });
return {
...toRefs(state),
}
},
})
</script>
<style lang="scss" scoped>
.page-box {
width: 100%;
background: #fff;
font-family: 'Arial', 'Microsoft YaHei', '黑体', sans-serif;
font-size: 14px;
.search {
padding: 20px;
// margin-bottom: 10px;
.head {
padding-bottom: 20px;
font-weight: bold;
display: flex;
align-items: center;
.icon {
display: inline-block;
width: 5px;
height: 16px;
background: #216fed;
margin-right: 5px;
}
}
.el-form {
display: flex;
align-items: flex-end;
flex-wrap: wrap;
padding: 20px;
// background: #f6f6f6;
border-radius: 5px;
}
.el-form-item {
margin-bottom: 20px;
}
::v-deep(.el-form-item__label) {
font-weight: bold;
}
.search-btn {
margin-left: auto;
}
::v-deep(.el-input-number .el-input__inner) {
text-align: left;
}
::v-deep(.el-input-group__prepend) {
background: #fff;
}
::v-deep(.el-range-separator) {
color: #b1b6c3;
}
}
.table {
padding: 20px 20px 0 20px;
background: #fff;
::v-deep(th) {
background: #f6f6f6;
color: rgba(0, 0, 0, 0.85);
}
::v-deep(th.el-table-fixed-column--right) {
background: #f6f6f6;
}
.head {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 20px;
font-weight: bold;
// background: #fff;
// .title {
// font-size: 14px;
// }
.toolbar {
display: flex;
flex-direction: row;
}
}
::v-deep(.cell) {
display: flex;
justify-content: center;
align-items: center;
}
.pagination {
display: flex;
justify-content: flex-end;
padding: 20px;
:last-child {
margin-right: 0;
}
}
}
}
</style>