785 lines
19 KiB
Vue
Raw Normal View History

2025-03-10 13:49:13 +08:00
<template>
<view class="z-table">
<view class="z-table-main" :style="compluteHeight">
<view v-if="!tableLoaded && (!tableData || !columns)" :class="['z-loading', {ztableLoading: tableShow}]">
<view class="z-loading-animate"></view>
</view>
<view class="z-table-container">
<view class="z-table-pack">
<view class="z-table-title">
<view class="z-table-title-item" :class="{ 'z-table-stick-side': stickSide && index == 0 }" :style="{ width: item.width ? item.width + 'rpx' : '200rpx' }"
v-for="(item, index) in columns" :key="index" @click="sort(item.key, index)">
<view v-if="showSelect && !singleSelect && index === 0" class="select-box" @click="doSelect(true)">
<view :class="['select-tip', {'selected': selectAll}]"></view>
</view>
<view :class="['z-table-col-text', {'text-left': titleTextAlign === 'left', 'text-center': titleTextAlign === 'center', 'text-right': titleTextAlign === 'right'}]">
<view v-html="getTitleText(item.title)"></view>
<view v-if="item.hasOwnProperty('key') && item.hasOwnProperty('sort') && tableData.length" class="sort">
<view class="up-arrow" :class="{ action: nowSortKey == item.key && sortType == 'asc' }"></view>
<view class="down-arrow" :class="{ action: nowSortKey == item.key && sortType == 'desc' }"></view>
</view>
</view>
</view>
</view>
<view v-if="tableData.length" :class="['table-container-box', {'short-table': !longTable && showBottomSum}]">
<view class="z-table-container-row" :class="{ 'z-table-has-bottom': showBottomSum }" v-for="(row, iIndex) in tableData"
:key="iIndex">
<view :class="['z-table-container-col', { 'z-table-stick-side': stickSide && jIndex == 0 }]" :style="{ width: col.width ? col.width + 'rpx' : '200rpx' }"
v-for="(col, jIndex) in columns" :key="jIndex" @click="itemClick(row, col)">
<view v-if="showSelect && jIndex === 0" class="select-box" @click="doSelect(false, iIndex)">
<view :class="['select-tip', {'selected': selectArr.includes(iIndex)}]"></view>
</view>
<view :class="['z-table-col-text', {'text-left': textAlign === 'left', 'text-center': textAlign === 'center', 'text-right': textAlign === 'right'}]">
<view v-if="!col.isLink" v-html="getRowContent(row, col)">
<!-- <view v-if="!col.render" v-html="getRowContent(row, col)"></view> -->
<!-- <renderComponents v-else :row="row" :col="col" /> -->
</view>
<!-- #ifdef H5 -->
<router-link v-else-if="setUrl(row, col).indexOf('http') != 0" :to="setUrl(row, col)" v-html="getRowContent(row, col)"></router-link>
<a v-else-if="col.isLink" :href="setUrl(row, col)" v-html="getRowContent(row, col)"></a>
<!-- #endif -->
<!-- #ifndef H5 -->
<navigator v-else-if="col.isLink" :url="setUrl(row, col)" v-html="getRowContent(row, col)"></navigator>
<!-- #endif -->
</view>
</view>
</view>
</view>
<view :class="['z-table-bottom', {'long-table': longTable}]" v-if="showBottomSum && tableData.length">
<view class="z-table-bottom-col" :class="{ 'z-table-stick-side': stickSide && sumIndex == 0 }" :style="{ width: sumCol.width ? sumCol.width + 'rpx' : '200rpx' }"
v-for="(sumCol, sumIndex) in columns" :key="sumIndex">
<view class="z-table-bottom-text">
<!-- <view v-if="sumIndex != 0" class="z-table-bottom-text-title">{{ sumCol.title }}</view> -->
<text :class="{ sum: sumIndex == 0 }">{{ sumIndex == 0 ? '总计' : dosum(sumCol) }}</text>
</view>
</view>
</view>
</view>
</view>
<view v-if="tableData && tableData.length == 0 && !tableLoaded" class="table-empty">
<!-- image v-if="!showLoading" class="empty-img" src="../static/empty.png"></image -->
<view v-html="showLoading ? '' : emptyText"></view>
</view>
</view>
</view>
</template>
<script>
/*
* 表格使用
* 注意如果需要异步加载需要把tableData初始值设为false当没有数据的时候值为空数组
* props: tableData [Array | Boolean] | 表格数据 如果为false则显示loading
* columns [Array | Boolean] | 数据映射表 如果为false则显示loading 每列params => title(表头文字可以是html字符串模版), width(每列宽度) [, key(对应tableData的字段名) || format(自定义内容), sort(是否要排序), isLink(是否显示为超链接Object)]
* format格式: {template: 字符串模版用#key#表示需要被替换的数据,names: 对应template属性内要被替换的内容的key}
* isLink格式: {url: 链接地址, params: 地址带的参数Array[key|value, key|value, ...]每一项都是key和value以'|'链接,如果不带'|'默认键值同名
* listenerClick(是否监听点击事件Boolean)}
* stickSide Boolean | 是否固定右侧首栏 默认不显示
* showBottomSum Boolean | 是否显示底部统计 默认不显示
* showLoading Boolean | 是否首次加载首次加载不显示暂无数据内容
* emptyText String | 空数据显示的文字内容
* tableHeight Number | 设置表格高度会滚动
* sort Boolean | 开启排序
* showSelect Boolean | 开启选择
* singleSelect Boolean | 在开启选择的状态下是否开起单选
* textAlign String | 内容对齐方式 left center right
* titleTextAlign String | 表头对齐方式 left center right
*
* event: onSort | 排序事件 返回{key: 被排序列的字段名, type: 正序'asc'/倒序'desc'}
* onSelect | 选中时触发 返回选择的行的下标
* onClick | 单元格点击事件 返回点击单元格所属行的数据
*
* function: resetSort | 调用后重置排序 *注意:不会触发sort事件
*
* */
import Vue from 'vue'
// import tableRender from './table-render'
export default {
data() {
return {
version: '1.1.3',
nowSortKey: '',
sortType: 'desc', // asc/desc 升序/降序
longTable: true,
lineHeight: uni.upx2px(64),
tableLoaded: false,
tableShow: true,
selectAll: false,
selectArr: []
}
},
// mixin: [tableRender],
computed: {
compluteHeight() {
return this.tableHeight ?
'height: ' + uni.upx2px(this.tableHeight) + 'px' :
''
}
},
props: {
tableData: {
type: [Array, Boolean],
default () {
return false
}
},
columns: {
/*
*
* [{title: xxx, key: 当前列展示对象名, width: 列宽, render: function}]
*
* */
type: [Array, Boolean],
required: true
},
stickSide: {
type: Boolean,
default: false
},
showBottomSum: {
type: Boolean,
default: false
},
showLoading: {
type: Boolean,
default: true
},
emptyText: {
type: String,
default: '暂无数据'
},
tableHeight: {
type: [Number, Boolean],
default: 0
},
showSelect: {
type: Boolean,
default: false
},
singleSelect: {
type: Boolean,
default: false
},
textAlign: {
type: String,
default: 'left' // right|center|left
},
titleTextAlign: {
type: String,
default: 'left' // right|center|left
}
},
mounted() {
this.init()
},
// components: {
// renderComponents: {
// functional: true,
// props: {
// row: {
// type: Object,
// required: true
// },
// col: {
// type: Object,
// required: true
// }
// },
// render: function(h, ctx) {
// return _this[ctx.props.col.render](h, ctx.props)
// }
// }
// },
watch: {
columns() {
this.init()
},
tableData() {
this.init()
}
},
methods: {
async init() {
// 重置选择内容
this.selectAll = false
this.selectArr = []
this.tableLoaded = false
this.tableShow = true
let _this = this
let container = await _this.getPageSize('.z-table-container'),
pack = await _this.getPageSize('.z-table-pack')
_this.timer && clearTimeout(_this.timer)
if (container && pack) {
_this.$nextTick(function() {
if (_this.tableData && _this.tableData.length) {
_this.tableShow = false
_this.timer = setTimeout(function() {
_this.tableLoaded = true
}, 300)
}
})
if (container.height != pack.height) {
_this.longTable = true
} else {
_this.longTable = false
}
} else {
_this.tableLoaded = false
_this.$nextTick(function() {
_this.tableShow = true
})
}
},
getPageSize(selecter) {
// 获取元素信息
let query = uni.createSelectorQuery().in(this),
_this = this
return new Promise((resolve, reject) => {
query
.select(selecter)
.boundingClientRect(res => {
resolve(res)
})
.exec()
})
},
dosum({key, noSum = false, formatNum = true}) {
let sum = '-'
if (noSum) return sum
if (this.tableData) {
if (
this.tableData.every(item => {
return !Number.isNaN(item[key] - 0)
})
) {
sum = 0
this.tableData.map((item, index) => {
if (!key && index != 0) {
sum = '-'
} else {
let val = item[key] - 0
if (Number.isNaN(val)) {
sum += 0
} else {
sum += val
}
}
})
}
}
// sum = sum == 0 ? "-" : sum
return formatNum ? this.numTransform(sum) : sum
},
getRowContent(row, col) {
// 表格值处理函数
// 如果columns带了key则显示对应的key
// 如果columns带的format则按规定返回format后的html
// format规定: params names <Array> 对应tableData的键名,作为匹配template中两个#之间动态内容的名字
// params template <String> html字符串模版
let tempHTML = ''
let rowKey = row[col.key]
if ([null, ''].includes(rowKey)) {
rowKey = '-'
}
let { formatNum = true } = col
if (rowKey || rowKey === 0) {
tempHTML = isNaN(rowKey - 0) || !formatNum ?
rowKey :
this.numTransform(rowKey - 0)
// tempHTML = tempHTML == 0 ? "-" : tempHTML
} else if (!!col.format) {
let tempFormat = col.format.template
col.format.names.map(item => {
let regexp = new RegExp(`\#${item}\#`, 'mg')
tempFormat = tempFormat.replace(regexp, row[item])
})
tempHTML = tempFormat
} else if (!col.render) {
let error = new Error('数据的key或format值至少一个不为空')
throw error
}
// console.log(tempHTML)
return tempHTML.toString()
},
sort(key, index) {
if (!key || !this.columns[index].sort) {
return
}
// 排序功能: 如果点击的排序按钮是原先的 那么更改排序类型
// 如果点击的另一个排序按钮 那么选择当前排序并且排序类型改为降序(desc)
if (key != this.nowSortKey) {
this.nowSortKey = key
this.sortType = 'desc'
} else {
this.toggleSort()
}
this.$emit('onSort', {
key: this.nowSortKey,
type: this.sortType
})
},
toggleSort() {
this.sortType = this.sortType == 'asc' ? 'desc' : 'asc'
},
numTransform(n) {
if (Number.isNaN(n - 0)) {
return n
}
if (Math.abs(n) >= 100000000) {
n = Number((n / 100000000).toFixed(1)) + '亿'
} else if (Math.abs(n) >= 10000) {
n = Number((n / 10000).toFixed(1)) + '万'
}
return n.toString()
},
resetSort() {
// 重置排序状态
this.nowSortKey = ''
this.sortType = 'desc'
},
setUrl(row, col) {
if (!col.isLink) {
return
}
let urlParam = {}
let {
isLink: {
url,
params = []
}
} = col
params.forEach(item => {
if (~item.indexOf('|')) {
let temp = item.split('|')
urlParam[temp[0]] = row[temp[1]]
} else {
urlParam[item] = row[item]
}
})
url = this.setUrlParams(url, urlParam)
return url
},
setUrlParams(url, params) {
let tempUrl = url,
keyArr = Object.keys(params)
keyArr.forEach(item => {
tempUrl += `&${item}=${params[item]}`
})
tempUrl = tempUrl.replace(/\&/, '?')
return tempUrl
},
itemClick(row, col) {
if (col.listenerClick) {
this.$emit('onClick', row)
}
},
doSelect(isAll = false, index) {
let temp = new Set()
if (isAll) {
// 全选
if (!this.selectAll) {
for (let i = 0; i < this.tableData.length; i++) {
temp.add(i)
}
}
} else {
// if (!this.singleSelect) {
// this.selectArr.forEach(item => {
// temp.add(item)
// })
// }
this.selectArr.forEach(item => {
temp.add(item)
})
if (temp.has(index)) {
temp.delete(index)
} else {
if (this.singleSelect) {
temp.clear()
}
temp.add(index)
}
}
this.selectArr = Array.from(temp)
// console.log(this.selectArr)
if (this.selectArr.length == this.tableData.length) {
this.selectAll = true
} else {
this.selectAll = false
}
this.$emit('onSelect', this.selectArr)
},
// 1.1.1
getTitleText(title) {
// 自定义表头
let tempHTML = title
return tempHTML.toString()
}
}
}
</script>
<style lang="scss">
.navigator-hover {
background: transparent;
opacity: 1;
}
@mixin ellipsis($num: 1) {
overflow: hidden;
text-overflow: ellipsis;
@if $num==1 {
white-space: nowrap;
}
@else {
display: -webkit-box;
-webkit-line-clamp: $num;
/* autoprefixer: off */
-webkit-box-orient: vertical;
/* autoprefixer: on */
}
}
// 三角形
%triangle-basic {
content: '';
height: 0;
width: 0;
overflow: hidden;
}
@mixin triangle($direction, $size, $borderColor) {
@extend %triangle-basic;
@if $direction==top {
border-bottom: $size solid $borderColor;
border-left: $size dashed transparent;
border-right: $size dashed transparent;
border-top: 0;
}
@else if $direction==right {
border-left: $size solid $borderColor;
border-top: $size dashed transparent;
border-bottom: $size dashed transparent;
border-right: 0;
}
@else if $direction==bottom {
border-top: $size solid $borderColor;
border-left: $size dashed transparent;
border-right: $size dashed transparent;
border-bottom: 0;
}
@else if $direction==left {
border-right: $size solid $borderColor;
border-top: $size dashed transparent;
border-bottom: $size dashed transparent;
border-left: 0;
}
}
a {
text-decoration: none;
}
.z-table {
position: relative;
display: inline-block;
height: 100%;
min-height: 130rpx;
width: 100%;
background: #fff;
border: solid 2rpx #ccc;
font-size: $uni-font-size-sm;
box-sizing: border-box;
transform: translateZ(0);
.z-table-main {
height: 100%;
box-sizing: border-box;
}
.z-table-container {
height: 100%;
overflow: scroll;
box-sizing: border-box;
}
.z-table-pack {
position: relative;
min-height: 100%;
width: fit-content;
}
.z-table-title {
position: sticky;
top: 0;
height: 64rpx;
z-index: 1;
.z-table-title-item {
border-bottom: solid 1rpx #dbdbdb;
background: #f8f8f8;
}
.z-table-stick-side {
position: sticky;
top: 0;
left: 0;
border-right: solid 1rpx #dbdbdb;
box-sizing: border-box;
}
}
.table-container-box.short-table {
padding-bottom: 48rpx;
}
.z-table-title,
.z-table-container-row {
display: flex;
width: fit-content;
white-space: nowrap;
box-sizing: border-box;
.z-table-title-item,
.z-table-container-col {
@include ellipsis();
display: inline-flex;
padding: 0 16rpx;
height: 64rpx;
align-items: center;
line-height: 64rpx;
box-sizing: border-box;
}
}
.z-table-container-row {
z-index: 0;
border-bottom: solid 1rpx #f4f4f4;
box-sizing: border-box;
}
.z-table-stick-side {
position: sticky;
left: 0;
background: #f7f9ff;
border-right: solid 1rpx #dbdbdb;
box-sizing: border-box;
}
.z-table-bottom {
position: absolute;
bottom: 0;
z-index: 9;
display: flex;
justify-items: center;
width: fit-content;
background: #4298f7 !important;
color: #fff !important;
white-space: nowrap;
box-sizing: border-box;
&.long-table {
position: sticky;
}
.z-table-stick-side {
background: #4298f7 !important;
box-sizing: border-box;
}
.z-table-bottom-col {
display: inline-flex;
align-items: center;
text-align: center;
padding: 16rpx;
box-sizing: border-box;
}
.z-table-bottom-text {
line-height: 100%;
box-sizing: border-box;
}
.z-table-bottom-text-title {
margin-bottom: 10rpx;
font-size: 22rpx;
color: #aad0ff;
box-sizing: border-box;
}
.sum {
margin-left: 14rpx;
font-size: 28rpx;
box-sizing: border-box;
}
}
.table-empty {
position: absolute;
top: 64rpx;
height: 64rpx;
line-height: 64rpx;
width: 100%;
text-align: center;
}
.sort {
display: flex;
padding: 5rpx;
flex-direction: column;
justify-content: center;
.up-arrow {
@include triangle(top, 10rpx, #ccc);
display: block;
margin-bottom: 5rpx;
&.action {
@include triangle(top, 10rpx, #4298f7);
}
}
.down-arrow {
@include triangle(bottom, 10rpx, #ccc);
display: block;
&.action {
@include triangle(bottom, 10rpx, #4298f7);
}
}
}
// 1.0.5
.z-loading {
position: absolute;
top: 0;
left: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
background: #fff;
opacity: 0;
transition: all 0.3s;
&.ztableLoading {
opacity: 1;
}
.z-loading-animate {
position: relative;
display: inline-block;
width: 30rpx;
height: 30rpx;
margin-right: 20rpx;
border-radius: 100%;
border: solid 6rpx #ccc;
vertical-align: middle;
animation: rotate 1s ease-in-out infinite;
&::after {
content: '';
display: block;
position: absolute;
top: -10rpx;
z-index: 1;
background: #fff;
width: 20rpx;
height: 20rpx;
border-radius: 10rpx;
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
}
// 1.1.0
.select-box {
display: inline-block;
width: 26rpx;
height: 26rpx;
line-height: 14rpx;
margin-right: 15rpx;
border: solid 2rpx #4298f7;
border-radius: 4rpx;
background: #fff;
text-align: center;
}
.select-tip {
display: inline-block;
opacity: 0;
transform: rotate(90deg);
transition: all .3s;
&.selected {
position: relative;
top: 4rpx;
left: -4rpx;
height: 4rpx;
background: #4298f7;
width: 10rpx;
opacity: 1;
transform: rotate(45deg);
&:before,
&:after {
content: '';
position: absolute;
display: block;
height: 4rpx;
background: #4298f7;
}
&:before {
bottom: -2rpx;
left: -4rpx;
width: 8rpx;
transform: rotate(-90deg);
}
&:after {
bottom: 16rpx;
right: -16rpx;
width: 34rpx;
transform: rotate(-90deg);
}
}
}
// 1.1.1
.z-table-col-text {
display: flex;
width: 100%;
flex: 1;
justify-content: flex-start;
align-content: center;
&.text-center {
justify-content: center;
}
&.text-right {
justify-content: flex-end;
}
}
}
</style>