如标题,效果如下图:
我做的是移动端,表格太长,所以做左边固定列右边滑动,上图右边显示动态箭头是让用户知道表格可以滑动。
表格的思路:
- 写两个table
- 一个是整体的table,外面的div保持宽度固定,内部的table宽度大于外部才能滚动;
- 一个是左边固定列,这个固定
position:absolute
在左边,就是盖在大table上面;
下面table组件
// table.vue
<template>
<div>
<div class="product-list">
<!-- 可滑动的全表格 -->
<div class="table-scroll-container" ref="tableScrollBox">
<table class="table-scroll" border="0" cellpadding="0" cellspacing="0">
<tbody class="tbody">
<tr class="title-tr">
<th class="ta-c" v-for="(item,index) in headData" :key="index" :class="{ 'first-col': index == 0 }">
{{ item.name }}
</th>
</tr>
<template>
<tr class="tbody-content" v-for="(item, index) in bodyData" :key="index">
<td
class="info-box"
v-for="(colItem, colIndex) in headData"
:class="[{ 'first-col clearfix': colIndex == 0 },colItem.class]">
{{ item[colItem.prop] }}
</td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- 覆盖的左侧表格 -->
<div class="table-left-container" v-if="isTableLeft">
<table class="table-left" :class="{ 'norem-table-fixed-left': isLeft }" border="0" cellpadding="0" cellspacing="0">
<tbody class="tbody">
<tr class="title-tr">
<th class="first-col">{{ headData[0].name }}</th>
</tr>
<template>
<tr class="tbody-content" v-for="(item, index) in bodyData" :key="index">
<td class="info-box" v-if="headData[0].prop">{{ item[headData[0].prop] }}</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- 动态箭头 bodyData没有数据就不显示 -->
<div class="arrow" v-if="bodyData.length != 0 && !arrow_left"></div>
</div>
</template>
<script>
export default {
props: {
// 表头内容
headData: {
type: Array,
default: () => []
},
// 表格内容
bodyData: {
type: Array,
default: () => []
},
// 是否要固定左边列
isTableLeft: {
type: Boolean,
default: () => true
},
// 是否要动态箭头
arrow_left: {
type: Boolean,
default: () => false
}
},
data() {
return {
isLeft: false,
};
},
mounted() {
//监听表格滚动,表格滚动到左边0时候去掉左边固定列的阴影,滚动到大于0时候才显示左边固定列的列
this.$refs.tableScrollBox.addEventListener(
"scroll",
() => {
this.$refs.tableScrollBox.scrollLeft
if(this.$refs.tableScrollBox.scrollLeft > 0){
this.isLeft = true;
}else{
this.isLeft = false;
}
},
true
);
},
};
</script>
// 动态箭头
<style scoped lang="less">
.arrow {
display: block;
width: 10px;
height: 10px;
position: fixed;
top: 50%;
right: 0;
margin-left: -11px;
border: 3px solid transparent;
border-top: 3px solid @primary_color;
border-left: 3px solid @primary_color;
z-index: 99;
opacity: .8;
transform: rotate(313deg);
animation: arrow 1.5s infinite ease-in-out;
}
@keyframes arrow {
0% {
opacity:0.2;
transform:translate(0, 0px) rotate(313deg)
}
50% {
opacity:1;
transform:translate(-5px, 0) rotate(313deg)
}
100% {
opacity:0.2;
transform:translate(-10px, 0) rotate(313deg)
}
}
</style>
下面需要表格组件的vue文件
// userList.vue
<template>
<div>
<tableLocked
:headData="tableHead"
:bodyData="tableBody"
:isTableLeft='true'>
</tableLocked>
</div>
</template>
<script>
import tableLocked from './tableLocked.vue'
export default {
data() {
return {
// table表头
tableHead: [
{
name: '账号',
prop: 'user_name'
},
{
name: '内容1',
prop: 'user1'
},
{
name: '内容2',
prop: 'user2'
},
{
name: '内容3',
prop: 'user3',
},
{
name: '内容4',
prop: 'user4',
},
{
name: '内容5',
prop: 'user5'
},
{
name: '操作',
prop: ''
}
],
// table内容
tableBody: [],
}
},
components: {
tableLocked
}
}
</script>
排序的思路:
- 给需要排序的表头多两个字段,一个显示,一个传值(Asc/Desc)
- 排序事件把Asc/Desc和该表头的值给父组件传过去
userList.vue:需要排序的表头添加两个字段
table.vue组件:添加排序的元素
<span class="sort" v-if="item.isSort" @click="onSort(item)">
<i class="up" :class="{ active: item.SortType == 'Asc' }"></i>
<i class="down" :class="{ active: item.SortType == 'Desc' }"></i>
</span>
methods: {
// 排序
onSort(item){
for(var i = 0; i < this.headData.length; i++){
if(this.headData[i].prop != item.prop){
this.headData[i].SortType = ''
}
}
// 避免重复请求
if(item.SortType == ''){
item.SortType = 'Asc'
}else if(item.SortType == 'Asc'){
item.SortType = 'Desc'
}else{
item.SortType = ''
}
// 把该参数给父组件过去,并触发父组件排序事件
this.$emit('goSort', item)
}
},
userList.vue:排序事件中获取该值并请求/逻辑
methods: {
// 排序
sortClick (item){
console.log(item)
// 逻辑
// ...
},
},
操作的思路:
- 操作的prop字段为空,可以判断这个字段是否空,是空的话就是操作
- 这个字段空的时候使用< slot >来填充操作下面的元素
- 在table.vue触发父组件对应的事件函数
table.vue:添加下面红圈
<template>
<tr class="tbody-content" v-for="(item, index) in bodyData" :key="index">
<td
class="info-box"
v-for="(colItem, colIndex) in headData"
v-if="colItem.prop"
:class="[{ 'first-col clearfix': colIndex == 0 },colItem.class]">
{{ item[colItem.prop] }}
</td>
<td class="operation" :key="colIndex" v-else @click.stop.capture="doThis($event, item, index)">
<slot></slot>
</td>
</tr>
</template>
触发父组件对应的操作事件函数,并把参数传过去
methods: {
doThis(e, item, index) {
// e.target.dataset.func是父组件操作下面元素的属性data-func
this.$emit(e.target.dataset.func, item)
},
},
userList.vue:添加两个元素和两个事件,data-func这个是来方便子组件table.vue触发对应的事件函数
<tableLocked
:headData="tableHead"
:bodyData="tableBody"
:isTableLeft='true'
@goSort="sortClick"
@goModify="modifyClick"
@goDelete="deleteClick">
<span data-func="goModify">修改</span>
<span data-func="goDelete">删除</span>
</tableLocked>
methods: {
modifyClick (item) {
console.log("修改:" + item)
},
deleteClick (item) {
console.log("删除:" + item)
},
},
完成代码:
table.vue
<template>
<div>
<div class="product-list">
<!-- 可滑动的全表格 -->
<div class="table-scroll-container" ref="tableScrollBox">
<table class="table-scroll" border="0" cellpadding="0" cellspacing="0">
<tbody class="tbody">
<tr class="title-tr">
<th class="ta-c" v-for="(item,index) in headData" :key="index" :class="{ 'first-col': index == 0 }">
{{ item.name }}
<span class="sort" v-if="item.isSort" @click="onSort(item)">
<i class="up" :class="{ active: item.SortType == 'Asc' }"></i>
<i class="down" :class="{ active: item.SortType == 'Desc' }"></i>
</span>
</th>
</tr>
<template>
<tr class="tbody-content" v-for="(item, index) in bodyData" :key="index">
<td
class="info-box"
v-for="(colItem, colIndex) in headData"
v-if="colItem.prop"
:class="[{ 'first-col clearfix': colIndex == 0 },colItem.class]">
{{ item[colItem.prop] }}
</td>
<td class="operation" :key="colIndex" v-else @click.stop.capture="doThis($event, item)">
<slot></slot>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- 覆盖的左侧表格 -->
<div class="table-left-container" v-if="isTableLeft">
<table class="table-left" border="0" cellpadding="0" cellspacing="0" :class="{ 'norem-table-fixed-left': isLeft }">
<tbody class="tbody">
<tr class="title-tr">
<th class="first-col">{{ headData[0].name }}</th>
</tr>
<template>
<tr class="tbody-content" v-for="(item, index) in bodyData" :key="index">
<td class="info-box" v-if="headData[0].prop">{{ item[headData[0].prop] }}</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- 动态箭头 -->
<div class="arrow" v-if="bodyData.length != 0 && !arrow_left"></div>
</div>
</template>
<script>
export default {
props: {
// 表头内容
headData: {
type: Array,
default: () => []
},
// 表格内容
bodyData: {
type: Array,
default: () => []
},
// 是否要固定左边列
isTableLeft: {
type: Boolean,
default: () => true
},
tdSlot: {
type: Boolean,
default: () => false
},
// 是否要动态箭头
arrow_left: {
type: Boolean,
default: () => false
}
},
data() {
return {
isLeft: false,
};
},
methods: {
doThis(e, item) {
// e.target.dataset.func是父组件操作下面元素的属性data-func
this.$emit(e.target.dataset.func, item)
},
// 排序
onSort(item){
for(var i = 0; i < this.headData.length; i++){
if(this.headData[i].prop != item.prop){
this.headData[i].SortType = ''
}
}
// 避免重复请求
if(item.SortType == ''){
item.SortType = 'Asc'
}else if(item.SortType == 'Asc'){
item.SortType = 'Desc'
}else{
item.SortType = ''
}
// 把该数据给父组件过去,并触发父组件排序事件
this.$emit('goSort', item)
}
},
mounted() {
this.$refs.tableScrollBox.addEventListener(
"scroll",
() => {
this.$refs.tableScrollBox.scrollLeft
if(this.$refs.tableScrollBox.scrollLeft > 0){
this.isLeft = true;
}else{
this.isLeft = false;
}
},
true
);
},
};
</script>
@primary_color: #617DEF;
.product-list{
position: relative;
margin-top: 16px;
overflow: hidden;
margin-bottom: 20px;
.table-left{
position: absolute;
top: 0;
left: 0;
z-index: 2;
background: #fff;
border-collapse: collapse;
text-align: left;
.tbody-content {
&:nth-child(2n+3) td{
background-color: #F8F8F8;
}
td {
height: 40px;
text-align: center;
box-sizing: border-box;
}
}
}
.table-scroll-container {
overflow-x: scroll;
overflow-y: hidden;
width: 100%;
}
.table-scroll{
width: 472px;
border-collapse: collapse;
text-align: left;
background-color: #fff;
.tbody-content:nth-child(2n+3){
background-color: #F8F8F8;
}
}
.title-tr{
height:17px;
font-size:12px;
font-weight:400;
color: #666;
line-height:17px;
border-bottom: 1px #E9E9E9 solid;
.first-col {
height: 32px;
border-bottom: 0;
}
}
.class-tr {
background-color: #fff;
width:48px;
height:24px;
font-size:12px;
line-height:20px;
box-sizing: border-box;
}
.first-col {
height: 40px;
box-sizing: border-box;
.first-col-cont {
width: 100px;
}
}
.info-box {
height: 40px;
text-align: center;
box-sizing: border-box;
}
div::-webkit-scrollbar {
width: 0;
height: 0;
}
th,td{
word-break: keep-all;
white-space:nowrap;
padding: 0 14px;
text-align: center;
}
span{
// color: @primary_color;
padding: 0 8px;
font-size: 12px;
}
th{
/deep/ i{
color: #a6a6a6;
vertical-align: text-top;
}
.sort{
padding: 0;
position: relative;
i{
color: #a6a6a6;
}
.up{
position: absolute;
top: -2px;
transform:rotate(-90deg);
-ms-transform:rotate(-90deg); /* IE 9 */
-moz-transform:rotate(-90deg); /* Firefox */
-webkit-transform:rotate(-90deg); /* Safari 和 Chrome */
-o-transform:rotate(-90deg);
&.active{
color: @primary_color;
}
}
.down{
position: absolute;
bottom: -2px;
transform:rotate(90deg);
-ms-transform:rotate(90deg); /* IE 9 */
-moz-transform:rotate(90deg); /* Firefox */
-webkit-transform:rotate(90deg); /* Safari 和 Chrome */
-o-transform:rotate(90deg);
&.active{
color: @primary_color;
}
}
}
}
.operation {
/deep/ span{
color: @primary_color;
padding: 0 8px;
font-size: 12px;
}
}
.ta-c {
text-align: center;
}
.norem-table-fixed-left{
box-shadow: 1px -3px 6px #00000029;
}
.add{
font-size: 16px;
vertical-align: text-top;
color: @primary_color;
}
.arrow {
display: block;
width: 10px;
height: 10px;
position: fixed;
top: 50%;
right: 0;
margin-left: -11px;
border: 3px solid transparent;
border-top: 3px solid @primary_color;
border-left: 3px solid @primary_color;
z-index: 99;
opacity: .8;
transform: rotate(313deg);
animation: arrow 1.5s infinite ease-in-out;
}
@keyframes arrow {
0% {
opacity:0.2;
transform:translate(0, 0px) rotate(313deg)
}
50% {
opacity:1;
transform:translate(-5px, 0) rotate(313deg)
}
100% {
opacity:0.2;
transform:translate(-10px, 0) rotate(313deg)
}
}
}
userList.vue
<template>
<div>
<tableLocked
:headData="tableHead"
:bodyData="tableBody"
:isTableLeft='true'
@goSort="sortClick"
@goModify="modifyClick"
@goDelete="deleteClick">
<span data-func="goModify">修改</span>
<span data-func="goDelete">删除</span>
</tableLocked>
</div>
</template>
<script>
import tableLocked from './tableLocked.vue'
export default {
data() {
return {
// table表头
tableHead: [
{
name: '账号',
prop: 'user_name'
},
{
name: '内容1',
prop: 'user1'
},
{
name: '内容2',
prop: 'user2'
},
{
name: '内容3',
prop: 'user3',
isSort: true,
SortType: ""
},
{
name: '内容4',
prop: 'user4',
isSort: true,
SortType: ""
},
{
name: '内容5',
prop: 'user5'
},
{
name: '操作',
prop: ''
},
],
// table内容
tableBody: [],
}
},
methods: {
modifyClick (item) {
console.log("修改:" + item)
},
deleteClick (item) {
console.log("删除:" + item)
},
// 排序
sortClick (item){
console.log(item)
// 请求时候把这个data数据传过去
// ...
},
},
components: {
tableLocked
}
}
</script>