feat: add AdvanceTable.vue component;

新增:高级表格;
master
chenghongxing 5 years ago
parent 3d3e56de12
commit 517c1959d8
  1. 166
      src/components/table/advance/ActionColumns.vue
  2. 44
      src/components/table/advance/ActionSize.vue
  3. 239
      src/components/table/advance/AdvanceTable.vue
  4. 262
      src/components/table/advance/SearchArea.vue
  5. 2
      src/components/table/advance/index.js
  6. 168
      src/pages/components/Table.vue
  7. 5
      src/router/config.js

@ -0,0 +1,166 @@
<template>
<div class="action-columns" ref="root">
<a-tooltip title="列设置" :get-popup-container="() => $refs.root">
<a-popover v-model="visible" placement="bottomRight" trigger="click" :get-popup-container="() => $refs.root">
<div slot="title">
<a-checkbox :indeterminate="indeterminate" :checked="checkAll" @change="onCheckAllChange" class="check-all" />列展示
<a-button @click="resetColumns" style="float: right" type="link" size="small">重置</a-button>
</div>
<a-list style="width: 100%" size="small" :key="i" v-for="(col, i) in columns" slot="content">
<a-list-item>
<a-checkbox v-model="col.visible" @change="e => onCheckChange(e, col)"/>
<template v-if="col.title">
{{col.title}}:
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<template slot="actions">
<a-tooltip title="固定在列头" :get-popup-container="() => $refs.root">
<a-icon :class="['left', {active: col.fixed === 'left'}]" @click="fixColumn('left', col)" type="vertical-align-top" />
</a-tooltip>
<a-tooltip title="固定在列尾" :get-popup-container="() => $refs.root">
<a-icon :class="['right', {active: col.fixed === 'right'}]" @click="fixColumn('right', col)" type="vertical-align-bottom" />
</a-tooltip>
<a-tooltip title="添加搜索" :get-popup-container="() => $refs.root">
<a-icon :class="{active: col.searchAble}" @click="setSearch(col)" type="search" />
</a-tooltip>
</template>
</a-list-item>
</a-list>
<a-icon class="action" type="setting" />
</a-popover>
</a-tooltip>
</div>
</template>
<script>
import cloneDeep from 'lodash.clonedeep'
export default {
name: 'ActionColumns',
props: ['columns', 'visibleColumns'],
data() {
return {
visible: false,
indeterminate: false,
checkAll: true,
checkedCounts: this.columns.length,
backColumns: cloneDeep(this.columns)
}
},
watch: {
checkedCounts(val) {
this.checkAll = val === this.columns.length
this.indeterminate = val > 0 && val < this.columns.length
}
},
created() {
this.$emit('update:visibleColumns', [...this.columns])
for (let col of this.columns) {
if (col.visible === undefined) {
this.$set(col, 'visible', true)
}
if (!col.visible) {
this.checkedCounts -= 1
this.$set(col, 'colSpan', 0)
this.$set(col, 'customCell', () => ({style: 'display: none;'}))
}
}
},
methods: {
onCheckChange(e, col) {
if (!col.visible) {
this.checkedCounts -= 1
this.$set(col, 'colSpan', 0)
this.$set(col, 'customCell', () => ({style: 'display: none;'}))
} else {
this.checkedCounts += 1
this.$set(col, 'colSpan', undefined)
this.$set(col, 'customCell', undefined)
}
},
fixColumn(fixed, col) {
if (fixed !== col.fixed) {
this.$set(col, 'fixed', fixed)
} else {
this.$set(col, 'fixed', undefined)
}
},
setSearch(col) {
this.$set(col, 'searchAble', !col.searchAble)
if (!col.searchAble && col.search) {
this.resetSearch(col)
}
},
resetSearch(col) {
col.search.value = col.dataType === 'boolean' ? false : undefined
col.search.backup = undefined
},
resetColumns() {
const {columns, backColumns} = this
let counts = columns.length
backColumns.forEach((back, index) => {
const column = columns[index]
column.visible = back.visible === undefined || back.visible
if (column.visible) {
this.$set(column, 'colSpan', undefined)
this.$set(column, 'customCell', undefined)
} else {
counts -= 1
this.$set(column, 'colSpan', 0)
this.$set(column, 'customCell', () => ({style: 'display: none;'}))
}
if (back.fixed !== undefined) {
column.fixed = back.fixed
} else {
this.$set(column, 'fixed', undefined)
}
column.searchAble = back.searchAble
this.resetSearch(column)
})
this.checkedCounts = counts
this.visible = false
this.$emit('reset', this.getConditions(columns))
},
onCheckAllChange(e) {
if (e.target.checked) {
this.checkedCounts = this.columns.length
this.columns.forEach(col => {
col.visible = true
this.$set(col, 'colSpan', undefined)
this.$set(col, 'customCell', undefined)
})
} else {
this.checkedCounts = 0
this.columns.forEach(col => {
col.visible = false
this.$set(col, 'colSpan', 0)
this.$set(col, 'customCell', () => ({style: 'display: none;'}))
})
}
},
getConditions(columns) {
const conditions = {}
columns.filter(item => item.search.value !== undefined && item.search.value !== '' && item.search.value !== null)
.forEach(col => {
conditions[col.dataIndex] = col.search.value
})
return conditions
}
}
}
</script>
<style scoped lang="less">
.action-columns{
display: inline-block;
.check-all{
margin-right: 8px;
}
.left,.right{
transform: rotate(-90deg);
}
.active{
color: @primary-color;
}
}
</style>

@ -0,0 +1,44 @@
<template>
<div class="action-size" ref="root">
<a-tooltip title="密度">
<a-dropdown placement="bottomCenter" :trigger="['click']" :get-popup-container="() => $refs.root">
<a-icon class="action" type="column-height" />
<a-menu :selected-keys="[value]" slot="overlay" @click="onClick">
<a-menu-item key="default">
默认
</a-menu-item>
<a-menu-item key="middle">
中等
</a-menu-item>
<a-menu-item key="small">
紧密
</a-menu-item>
</a-menu>
</a-dropdown>
</a-tooltip>
</div>
</template>
<script>
export default {
name: 'ActionSize',
props: ['value'],
inject: ['table'],
data() {
return {
selectedKeys: ['middle']
}
},
methods: {
onClick({key}) {
this.$emit('input', key)
}
}
}
</script>
<style scoped lang="less">
.action-size{
display: inline-block;
}
</style>

@ -0,0 +1,239 @@
<template>
<div ref="table" :id="id" class="advanced-table">
<a-spin :spinning="loading">
<div :class="['header-bar', size]">
<div class="title">
<template v-if="title">{{title}}</template>
<slot v-else-if="$slots.title" name="title"></slot>
<template v-else>高级表格</template>
</div>
<div class="search">
<search-area @change="onSearchChange" :columns="columns" >
<template :slot="slot" v-for="slot in slots">
<slot :name="slot"></slot>
</template>
</search-area>
</div>
<div class="actions">
<a-tooltip title="刷新">
<a-icon @click="refresh" class="action" :type="loading ? 'loading' : 'reload'" />
</a-tooltip>
<action-size v-model="sSize" class="action" />
<action-columns :columns="columns" @reset="onColumnsReset" class="action">
<template :slot="slot" v-for="slot in slots">
<slot :name="slot"></slot>
</template>
</action-columns>
<a-tooltip title="全屏">
<a-icon @click="toggleScreen" class="action" :type="fullScreen ? 'fullscreen-exit' : 'fullscreen'" />
</a-tooltip>
</div>
</div>
<a-table
v-bind="{...$options.propsData, title: undefined, loading: false}"
:size="sSize"
@expandedRowsChange="onExpandedRowsChange"
@change="onChange"
@expand="onExpand"
>
<template slot-scope="text, record, index" :slot="slot" v-for="slot in scopedSlots ">
<slot :name="slot" v-bind="{text, record, index}"></slot>
</template>
<template :slot="slot" v-for="slot in slots">
<slot :name="slot"></slot>
</template>
<template slot-scope="record, index, indent, expanded" :slot="$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''">
<slot v-bind="{record, index, indent, expanded}" :name="$scopedSlots.expandedRowRender ? 'expandedRowRender' : ''"></slot>
</template>
</a-table>
</a-spin>
</div>
</template>
<script>
import ActionSize from '@/components/table/advance/ActionSize'
import ActionColumns from '@/components/table/advance/ActionColumns'
import SearchArea from '@/components/table/advance/SearchArea'
export default {
name: 'AdvanceTable',
components: {SearchArea, ActionColumns, ActionSize},
props: {
tableLayout: String,
bordered: Boolean,
childrenColumnName: Array[String],
columns: Array,
components: Object,
dataSource: Array,
defaultExpandAllRows: Array[String],
expandedRowKeys: Array[String],
expandedRowRender: Function,
expandIcon: Function,
expandRowByClick: Boolean,
expandIconColumnIndex: Number,
footer: Function,
indentSize: Number,
loading: Boolean,
locale: Object,
pagination: Object,
rowClassName: Function,
rowKey: [String, Function],
rowSelection: Object,
scroll: Object,
showHeader: Boolean,
size: String,
title: String,
customHeaderRow: Function,
customRow: Function,
getPopupContainer: Function,
transformCellText: Function
},
provide() {
return {
table: this
}
},
data() {
return {
id: `${new Date().getTime()}-${Math.floor(Math.random() * 10)}`,
sSize: this.size || 'default',
fullScreen: false,
conditions: []
}
},
computed: {
slots() {
return Object.keys(this.$slots).filter(slot => slot !== 'title')
},
scopedSlots() {
return Object.keys(this.$scopedSlots).filter(slot => slot !== 'expandedRowRender' && slot !== 'title')
}
},
created() {
this.addListener()
},
beforeDestroy() {
this.removeListener()
},
methods: {
refresh() {
this.$emit('refresh', this.conditions)
},
onSearchChange(conditions) {
this.conditions = conditions
this.$emit('search', conditions)
},
toggleScreen() {
if (this.fullScreen) {
this.outFullScreen()
} else {
this.inFullScreen()
}
},
inFullScreen() {
const el = this.$refs.table
if (el.requestFullscreen) {
el.requestFullscreen()
return true
} else if (el.webkitRequestFullScreen) {
el.webkitRequestFullScreen()
return true
} else if (el.mozRequestFullScreen) {
el.mozRequestFullScreen()
return true
} else if (el.msRequestFullscreen) {
el.msRequestFullscreen()
return true
}
this.$message.warn('对不起,您的浏览器不支持全屏模式')
return false
},
outFullScreen() {
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen()
} else if (document.msExitFullscreen) {
document.msExiFullscreen()
}
},
onColumnsReset(conditions) {
this.$emit('reset', conditions)
},
onExpandedRowsChange(expandedRows) {
this.$emit('expandedRowsChange', expandedRows)
},
onChange(pagination, filters, sorter, options) {
this.$emit('expandedRowsChange', pagination, filters, sorter, options)
},
onExpand(expanded, record) {
this.$emit('expandedRowsChange', expanded, record)
},
addListener() {
document.addEventListener('fullscreenchange', this.fullScreenListener)
document.addEventListener('webkitfullscreenchange', this.fullScreenListener)
document.addEventListener('mozfullscreenchange', this.fullScreenListener)
document.addEventListener('msfullscreenchange', this.fullScreenListener)
},
removeListener() {
document.removeEventListener('fullscreenchange', this.fullScreenListener)
document.removeEventListener('webkitfullscreenchange', this.fullScreenListener)
document.removeEventListener('mozfullscreenchange', this.fullScreenListener)
document.removeEventListener('msfullscreenchange', this.fullScreenListener)
},
fullScreenListener(e) {
if (e.target.id === this.id) {
this.fullScreen = !this.fullScreen
}
}
}
}
</script>
<style scoped lang="less">
.advanced-table{
background-color: @component-background;
.header-bar{
padding: 16px 24px;
display: flex;
align-items: center;
border-radius: 4px;
transition: all 0.3s;
&.middle{
padding: 12px 16px;
}
&.small{
padding: 8px 12px;
border: 1px solid @border-color;
border-bottom: 0;
.title{
font-size: 16px;
}
}
.title{
transition: all 0.3s;
font-size: 18px;
color: @title-color;
font-weight: 700;
}
.search{
flex: 1;
text-align: right;
margin: 0 24px;
}
.actions{
text-align: right;
font-size: 17px;
color: @text-color;
.action{
margin: 0 8px;
cursor: pointer;
&:hover{
color: @primary-color;
}
}
}
}
}
</style>

@ -0,0 +1,262 @@
<template>
<div class="search-area" ref="root">
<div class="search-item" :key="index" v-for="(col, index) in searchCols">
<div v-if="col.dataType === 'boolean'" class="title active">
<template v-if="col.title">
{{col.title}}:
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<a-switch @change="onSwitchChange" class="switch" v-model="col.search.value" size="small" checked-children="" un-checked-children="" />
</div>
<div v-else-if="col.dataType === 'time'" class="title active">
<template v-if="col.title">
{{col.title}}:
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<a-time-picker v-model="col.search.value" placeholder="选择时间" @change="(time, timeStr) => onCalendarChange(time, timeStr, col)" @openChange="open => onCalendarOpenChange(open, col)" class="time-picker" size="small" />
</div>
<div v-else-if="col.dataType === 'date'" class="title active">
<template v-if="col.title">
{{col.title}}:
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<a-date-picker v-model="col.search.value" @change="onDateChange(col)" class="date-picker" size="small" />
</div>
<div v-else-if="col.dataType === 'datetime'" class="title datetime active">
<template v-if="col.title">
{{col.title}}:
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<a-date-picker v-model="col.search.value" @change="(date, dateStr) => onCalendarChange(date, dateStr, col)" @openChange="open => onCalendarOpenChange(open, col)" show-time class="datetime-picker" size="small" />
</div>
<div v-else-if="col.dataType === 'select'" class="title active">
<template v-if="col.title">
{{col.title}}:
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<a-select :allowClear="true" :options="col.search.selectOptions" v-model="col.search.value" placeholder="请选择..." @change="onSelectChange(col)" class="select" slot="content" size="small">
</a-select>
</div>
<a-popover v-else @visibleChange="onVisibleChange(col, index)" v-model="col.search.visible" placement="bottom" :trigger="['click']" :get-popup-container="() => $refs.root">
<div :class="['title', {active: col.search.value}]">
<template v-if="col.title">
{{col.title}}
</template>
<slot v-else-if="col.slots && col.slots.title" :name="col.slots.title"></slot>
<div class="value " v-if="col.search.value">:&nbsp;&nbsp;{{col | searchValue}}</div>
<a-icon class="icon-down" type="down"/>
</div>
<div class="operations" slot="content">
<a-button @click="onCancel(col)" class="btn" size="small" type="link">取消</a-button>
<a-button @click="onConfirm(col)" class="btn" size="small" type="primary">确认</a-button>
</div>
<div class="search-overlay" slot="title">
<a-input :id="`${searchIdPrefix}${index}`" :allow-clear="true" @keyup.esc="onCancel(col)" @keyup.enter="onConfirm(col)" v-model="col.search.value" size="default" />
</div>
</a-popover>
</div>
</div>
</template>
<script>
import fastEqual from 'fast-deep-equal'
import moment from 'moment'
export default {
name: 'SearchArea',
props: ['columns'],
inject: ['table'],
created() {
this.columns.forEach(item => {
this.$set(item, 'search', {...item.search, visible: false, value: item.dataType === 'boolean' ? false : undefined, format: this.getCalendarFormat(item)})
})
},
filters: {
searchValue(col) {
if (col.dataType === 'time' && col.search.value) {
return col.search.value.format('HH:mm:ss')
}
return col.search.value
}
},
watch: {
searchCols(newVal, oldVal) {
if (newVal.length != oldVal.length) {
const newConditions = this.getConditions(newVal)
if (!fastEqual(newConditions, this.conditions)) {
this.conditions = newConditions
this.$emit('change', this.conditions)
}
}
}
},
data() {
return {
conditions: []
}
},
computed: {
searchCols() {
return this.columns.filter(item => item.searchAble)
},
searchIdPrefix() {
return this.table.id + '-ipt-'
}
},
methods: {
onCancel(col) {
col.search.value = col.search.backup
col.search.visible = false
},
onConfirm(col) {
col.search.backup = col.search.value
col.search.visible = false
const conditions = this.getConditions(this.searchCols)
if (!fastEqual(conditions, this.conditions)) {
this.conditions = conditions
this.$emit('change', this.conditions)
}
},
onSwitchChange() {
this.conditions = this.getConditions(this.searchCols)
this.$emit('change', this.conditions)
},
onSelectChange() {
this.conditions = this.getConditions(this.searchCols)
this.$emit('change', this.conditions)
},
onCalendarOpenChange(open, col) {
col.search.visible = open
const {momentEqual, getConditions} = this
const {value, backup, format} = col.search
if (!open && !momentEqual(value, backup, format)) {
col.search.backup = moment(value)
this.conditions = getConditions(this.searchCols)
this.$emit('change', this.conditions)
}
},
onCalendarChange(date, dateStr, col) {
const {momentEqual, getConditions} = this
const {value, backup, format} = col.search
if (!col.search.visible && !momentEqual(value, backup, format)) {
col.search.backup = moment(value)
this.conditions = getConditions(this.searchCols)
this.$emit('change', this.conditions)
}
},
onDateChange(col) {
const {momentEqual, getConditions} = this
const {value, backup} = col.search
if (!momentEqual(value, backup, 'YYYY-MM-DD')) {
col.search.backup = moment(value)
this.conditions = getConditions(this.searchCols)
this.$emit('change', this.conditions)
}
},
getCalendarFormat(col) {
const dataType = col.dataType
switch(dataType) {
case 'time': return 'HH:mm:ss'
case 'date': return 'YYYY-MM-DD'
case 'datetime': return 'YYYY-MM-DD HH:mm:ss'
default: return col.search && col.search.format
}
},
getConditions(columns) {
const conditions = {}
columns.filter(item => item.search.value !== undefined && item.search.value !== '' && item.search.value !== null)
.forEach(col => {
conditions[col.dataIndex] = col.search.value
})
return conditions
},
onVisibleChange(col, index) {
if (!col.search.visible) {
col.search.value = col.search.backup
} else {
let input = document.getElementById(`${this.searchIdPrefix}${index}`)
if (input) {
setTimeout(() => {input.focus()}, 0)
} else {
this.$nextTick(() => {
input = document.getElementById(`${this.searchIdPrefix}${index}`)
input.focus()
})
}
}
},
momentEqual(target, source, format) {
if (target === source) {
return true
} else if (target && source && target.format(format) === source.format(format)) {
return true
}
return false
}
}
}
</script>
<style scoped lang="less">
.search-area{
margin: -4px 0;
.search-item{
margin: 4px 4px;
display: inline-block;
.title{
padding: 4px 8px;
cursor: pointer;
border-radius: 4px;
user-select: none;
display: inline-flex;
align-items: center;
.switch{
margin-left: 4px;
}
.time-picker{
margin-left: 4px;
width: 96px;
}
.date-picker{
margin-left: 4px;
width: 120px;
}
.datetime-picker{
margin-left: 4px;
width: 195px;
}
.value{
display: inline-block;
overflow: hidden;
flex:1;
max-width: 144px;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
}
&.active{
background-color: @layout-bg-color;
}
}
.icon-down{
margin-left: 4px;
font-size: 12px;
}
}
.search-overlay{
padding: 8px 0px;
text-align: center;
}
.select{
margin-left: 4px;
max-width: 144px;
min-width: 96px;
}
.operations{
display: flex;
justify-content: space-between;
.btn{
}
}
}
</style>

@ -0,0 +1,2 @@
import AdvanceTable from './AdvanceTable'
export default AdvanceTable

@ -0,0 +1,168 @@
<template>
<div>
<advance-table
:columns="columns"
:data-source="dataSource"
title="高级表格-Beta"
:loading="loading"
rowKey="id"
@search="onSearch"
@refresh="onRefresh"
@reset="onReset"
>
<template slot="statusTitle">
状态<a-icon style="margin: 0 4px" type="info-circle" />
</template>
<template slot="send" slot-scope="{text}">
{{text ? '是' : '否'}}
</template>
<template slot="status" slot-scope="{text}">
{{text | statusStr}}
</template>
</advance-table>
</div>
</template>
<script>
import AdvanceTable from '@/components/table/advance/AdvanceTable'
import moment from 'moment'
const goods = ['运动鞋', 'T恤', '长裤', '短裤']
const dataSource = []
const current = new Date().getTime()
for (let i = 0; i < 100; i++) {
dataSource.push({
id: i,
name: goods[Math.floor((Math.random() * 4))],
orderId: `${new Date().getTime()}-${Math.floor(Math.random() * 10)}`,
status: Math.floor((Math.random() * 4) + 1),
send: (i % 2) === 1,
sendTime: moment(current - Math.floor((Math.random() * 8000000))).format('YYYY-MM-DD HH:mm:ss'),
orderDate: moment(current - Math.floor((Math.random() * 800000000))).format('YYYY-MM-DD'),
auditTime: moment(current - Math.floor((Math.random() * 8000000))).format('HH:mm:ss'),
})
}
export default {
name: 'Table',
components: {AdvanceTable},
filters: {
statusStr(val) {
switch (val) {
case 1: return '已下单'
case 2: return '已付款'
case 3: return '已审核'
case 4: return '已发货'
}
}
},
data() {
return {
loading: false,
columns: [
{
title: '商品名称',
dataIndex: 'name',
searchAble: true
},
{
title: '订单号',
dataIndex: 'orderId'
},
{
searchAble: true,
dataIndex: 'status',
dataType: 'select',
slots: {title: 'statusTitle'},
scopedSlots: {customRender: 'status'},
search: {
selectOptions: [
{title: '已下单', value: 1},
{title: '已付款', value: 2},
{title: '已审核', value: 3},
{title: '已发货', value: 4}
]
}
},
{
title: '发货',
searchAble: true,
dataIndex: 'send',
dataType: 'boolean',
scopedSlots: {customRender: 'send'}
},
{
title: '发货时间',
dataIndex: 'sendTime',
dataType: 'datetime'
},
{
title: '下单日期',
searchAble: true,
dataIndex: 'orderDate',
dataType: 'date'
},
{
title: '审核时间',
searchAble: true,
dataIndex: 'auditTime',
dataType: 'time',
},
],
dataSource: dataSource
}
},
methods: {
onSearch(conditions) {
this.loading = true
this.searchGoods(conditions).then(result => {
this.dataSource = result
this.loading = false
})
},
onRefresh(conditions) {
this.loading = true
this.searchGoods(conditions).then(result => {
this.dataSource = result
this.loading = false
})
},
onReset(conditions) {
this.loading = true
this.searchGoods(conditions).then(result => {
this.dataSource = result
this.loading = false
})
},
async searchGoods(conditions) {
const promise = new Promise((resolve, reject) => {
try {
const result = dataSource.filter(item => {
for (let key of Object.keys(conditions)) {
if (key === 'sendTime') {
if (conditions[key].format('YYYY-MM-DD HH:mm:ss') !== item[key]) return false
} else if (key === 'orderDate') {
if (conditions[key].format('YYYY-MM-DD') !== item[key]) return false
} else if (key === 'auditTime') {
if (conditions[key].format('HH:mm:ss') !== item[key]) return false
} else if (item[key] !== conditions[key]) {
return false
}
}
return true
})
setTimeout(() => {
resolve(result)
}, 300)
} catch (e) {
reject(e)
}
})
return promise
}
}
}
</script>
<style scoped>
</style>

@ -208,6 +208,11 @@ const options = {
path: 'palette',
name: '颜色复选框',
component: () => import('@/pages/components/Palette')
},
{
path: 'table',
name: '高级表格',
component: () => import('@/pages/components/Table')
}
]
},

Loading…
Cancel
Save