518 lines
16 KiB
Vue
518 lines
16 KiB
Vue
<template>
|
||
<view scroll-x class="u-table2" :style="{ height: height ? height + 'px' : 'auto' }">
|
||
<!-- 表头 -->
|
||
<view v-if="showHeader" class="u-table-header" :class="{ 'u-table-sticky': fixedHeader }" :style="{minWidth: scrollWidth}">
|
||
<view class="u-table-row">
|
||
<view v-for="(col, colIndex) in columns" :key="col.key" class="u-table-cell"
|
||
:style="headerColStyle(col)"
|
||
:class="[
|
||
col.align ? 'u-text-' + col.align : '',
|
||
headerCellClassName ? headerCellClassName(col) : '',
|
||
col.fixed === 'left' ? 'u-table-fixed-left' : '',
|
||
col.fixed === 'right' ? 'u-table-fixed-right' : ''
|
||
]" @click="handleHeaderClick(col)">
|
||
<slot name="header" :column="col" :columnIndex="colIndex" :level="1">
|
||
{{ col.title }}
|
||
</slot>
|
||
<view v-if="col.sortable">
|
||
{{ getSortIcon(col.key) }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 表体 -->
|
||
<view class="u-table-body" :style="{ minWidth: scrollWidth, maxHeight: maxHeight ? maxHeight + 'px' : 'none' }">
|
||
<template v-if="data && data.length > 0">
|
||
<template v-for="(row, index) in sortedData" :key="row[rowKey] || index">
|
||
<view class="u-table-row" :class="[
|
||
highlightCurrentRow && currentRow === row ? 'u-table-row-highlight' : '',
|
||
rowClassName ? rowClassName(row, index) : '',
|
||
stripe && index % 2 === 1 ? 'u-table-row-zebra' : ''
|
||
]" @click="handleRowClick(row)">
|
||
<view v-for="(col, colIndex) in columns" :key="col.key"
|
||
class="u-table-cell" :class="[
|
||
col.align ? 'u-text-' + col.align : '',
|
||
cellClassName ? cellClassName(row, col) : '',
|
||
col.fixed === 'left' ? 'u-table-fixed-left' : '',
|
||
col.fixed === 'right' ? 'u-table-fixed-right' : ''
|
||
]" :style="cellStyleInner({row: row, column: col,
|
||
rowIndex: index, columnIndex: colIndex, level: 0})">
|
||
<!-- 复选框列 -->
|
||
<view v-if="col.type === 'selection'">
|
||
<checkbox :checked="isSelected(row)"
|
||
@click.stop="toggleSelect(row)" />
|
||
</view>
|
||
|
||
<!-- 树形结构展开图标 -->
|
||
<view v-else-if="col.type === 'expand'"
|
||
@click.stop="toggleExpand(row)">
|
||
{{ isExpanded(row) ? '▼' : '▶' }}
|
||
</view>
|
||
|
||
<!-- 默认插槽或文本 -->
|
||
<slot name="cell" :row="row" :column="col"
|
||
:rowIndex="index" :columnIndex="colIndex">
|
||
<view class="u-table-cell_content">
|
||
{{ row[col.key] }}
|
||
</view>
|
||
</slot>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 子级渲染 -->
|
||
<template v-if="isExpanded(row) && row[treeProps.children] && row[treeProps.children].length">
|
||
<view v-for="childRow in row[treeProps.children]" :key="childRow[rowKey]"
|
||
class="u-table-row u-table-row-child">
|
||
<view v-for="(col2, col2Index) in columns" :key="col2.key" class="u-table-cell"
|
||
:style="cellStyleInner({row: childRow, column: col2,
|
||
rowIndex: index, columnIndex: col2Index, level: 1})">
|
||
<slot name="cell" :row="childRow" :column="col2" :prow="row"
|
||
:rowIndex="index" :columnIndex="col2Index" :level="1">
|
||
<view class="u-table-cell_content">
|
||
{{ childRow[col2.key] }}
|
||
</view>
|
||
</slot>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
</template>
|
||
</template>
|
||
<template v-else>
|
||
<slot name="empty">
|
||
<view class="u-table-empty">{{ emptyText }}</view>
|
||
</slot>
|
||
</template>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { ref, watch, computed } from 'vue'
|
||
import { addUnit, sleep } from '../../libs/function/index';
|
||
|
||
export default {
|
||
name: 'u-table2',
|
||
props: {
|
||
data: {
|
||
type: Array,
|
||
required: true,
|
||
default: () => {
|
||
return []
|
||
}
|
||
},
|
||
columns: {
|
||
type: Array,
|
||
required: true,
|
||
default: () => {
|
||
return []
|
||
},
|
||
validator: cols =>
|
||
cols.every(col =>
|
||
['default', 'selection', 'expand'].includes(col.type || 'default')
|
||
)
|
||
},
|
||
stripe: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
border: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
height: {
|
||
type: [String, Number],
|
||
default: null
|
||
},
|
||
maxHeight: {
|
||
type: [String, Number],
|
||
default: null
|
||
},
|
||
showHeader: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
highlightCurrentRow: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
rowKey: {
|
||
type: String,
|
||
default: 'id'
|
||
},
|
||
currentRowKey: {
|
||
type: [String, Number],
|
||
default: null
|
||
},
|
||
rowStyle: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
cellClassName: {
|
||
type: Function,
|
||
default: null
|
||
},
|
||
cellStyle: {
|
||
type: Function,
|
||
default: null
|
||
},
|
||
headerCellClassName: {
|
||
type: Function,
|
||
default: null
|
||
},
|
||
rowClassName: {
|
||
type: Function,
|
||
default: null
|
||
},
|
||
context: {
|
||
type: Object,
|
||
default: null
|
||
},
|
||
showOverflowTooltip: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
lazy: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
load: {
|
||
type: Function,
|
||
default: null
|
||
},
|
||
treeProps: {
|
||
type: Object,
|
||
default: () => ({
|
||
children: 'children',
|
||
hasChildren: 'hasChildren'
|
||
})
|
||
},
|
||
defaultExpandAll: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
expandRowKeys: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
sortOrders: {
|
||
type: Array,
|
||
default: () => ['ascending', 'descending']
|
||
},
|
||
sortable: {
|
||
type: [Boolean, String],
|
||
default: false
|
||
},
|
||
multiSort: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
sortBy: {
|
||
type: String,
|
||
default: null
|
||
},
|
||
sortMethod: {
|
||
type: Function,
|
||
default: null
|
||
},
|
||
filters: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
fixedHeader: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
emptyText: {
|
||
type: String,
|
||
default: '暂无数据'
|
||
},
|
||
},
|
||
emits: [
|
||
'select', 'select-all', 'selection-change',
|
||
'cell-click', 'row-click', 'row-dblclick',
|
||
'header-click', 'sort-change', 'filter-change',
|
||
'current-change', 'expand-change'
|
||
],
|
||
data() {
|
||
return {
|
||
scrollWidth: 'auto'
|
||
}
|
||
},
|
||
mounted() {
|
||
this.getComponentWidth()
|
||
},
|
||
computed: {
|
||
},
|
||
methods: {
|
||
addUnit,
|
||
headerColStyle(col) {
|
||
let style = {
|
||
width: col.width ? addUnit(col.width) : 'auto',
|
||
flex: col.width ? 'none' : 1
|
||
};
|
||
if (col?.style) {
|
||
style = {...style, ...col?.style};
|
||
}
|
||
return style;
|
||
},
|
||
setCellStyle(e) {
|
||
this.cellStyle = e
|
||
},
|
||
cellStyleInner(scope) {
|
||
let style = {
|
||
width: scope.column?.width ? addUnit(scope.column.width) : 'auto',
|
||
flex: scope.column?.width ? 'none' : 1,
|
||
paddingLeft: (24 * scope.level) + 'px'
|
||
};
|
||
if (this.cellStyle != null) {
|
||
let styleCalc = this.cellStyle(scope)
|
||
if (styleCalc != null) {
|
||
style = {...style, ...styleCalc}
|
||
}
|
||
}
|
||
return style;
|
||
},
|
||
// 获取组件的宽度
|
||
async getComponentWidth() {
|
||
// 延时一定时间,以获取dom尺寸
|
||
await sleep(30)
|
||
this.$uGetRect('.u-table-row').then(size => {
|
||
this.scrollWidth = size.width + 'px'
|
||
})
|
||
},
|
||
},
|
||
setup(props, { emit }) {
|
||
const expandedKeys = ref([...props.expandRowKeys]);
|
||
const selectedRows = ref([]);
|
||
const sortConditions = ref([]);
|
||
|
||
// 当前高亮行
|
||
const currentRow = ref(null);
|
||
|
||
watch(
|
||
() => props.expandRowKeys,
|
||
newVal => {
|
||
expandedKeys.value = [...newVal];
|
||
}
|
||
);
|
||
|
||
watch(
|
||
() => props.currentRowKey,
|
||
newVal => {
|
||
const found = props.data.find(item => item[props.rowKey] === newVal);
|
||
if (found) {
|
||
currentRow.value = found;
|
||
}
|
||
}
|
||
);
|
||
|
||
// 过滤后的数据
|
||
const filteredData = computed(() => {
|
||
return props.data.filter(row => {
|
||
return Object.keys(props.filters).every(key => {
|
||
const filter = props.filters[key];
|
||
if (!filter) return true;
|
||
return row[key]?.toString().includes(filter.toString());
|
||
});
|
||
});
|
||
});
|
||
|
||
// 排序后的数据
|
||
const sortedData = computed(() => {
|
||
if (!sortConditions.value.length) return filteredData.value;
|
||
|
||
const data = [...filteredData.value];
|
||
|
||
return data.sort((a, b) => {
|
||
for (const condition of sortConditions.value) {
|
||
const { field, order } = condition;
|
||
let valA = a[field];
|
||
let valB = b[field];
|
||
|
||
if (props.sortMethod) {
|
||
const result = props.sortMethod(a, b, field);
|
||
if (result !== 0) return result * (order === 'ascending' ? 1 : -1);
|
||
}
|
||
|
||
if (valA < valB) return order === 'ascending' ? -1 : 1;
|
||
if (valA > valB) return order === 'ascending' ? 1 : -1;
|
||
}
|
||
return 0;
|
||
});
|
||
});
|
||
|
||
function handleRowClick(row) {
|
||
if (props.highlightCurrentRow) {
|
||
const oldRow = currentRow.value;
|
||
currentRow.value = row;
|
||
emit('current-change', row, oldRow);
|
||
}
|
||
emit('row-click', row);
|
||
}
|
||
|
||
function handleHeaderClick(column) {
|
||
if (!column.sortable) return;
|
||
|
||
const index = sortConditions.value.findIndex(c => c.field === column.key);
|
||
let newOrder = 'ascending';
|
||
|
||
if (index >= 0) {
|
||
if (sortConditions.value[index].order === 'ascending') {
|
||
newOrder = 'descending';
|
||
} else {
|
||
sortConditions.value.splice(index, 1);
|
||
emit('sort-change', sortConditions.value);
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (!props.multiSort) {
|
||
sortConditions.value = [{ field: column.key, order: newOrder }];
|
||
} else {
|
||
if (index >= 0) {
|
||
sortConditions.value[index].order = newOrder;
|
||
} else {
|
||
sortConditions.value.push({ field: column.key, order: newOrder });
|
||
}
|
||
}
|
||
|
||
emit('sort-change', sortConditions.value);
|
||
}
|
||
|
||
function getSortIcon(field) {
|
||
const cond = sortConditions.value.find(c => c.field === field);
|
||
if (!cond) return '';
|
||
return cond.order === 'ascending' ? '↑' : '↓';
|
||
}
|
||
|
||
function toggleSelect(row) {
|
||
const index = selectedRows.value.findIndex(r => r[props.rowKey] === row[props.rowKey]);
|
||
if (index >= 0) {
|
||
selectedRows.value.splice(index, 1);
|
||
} else {
|
||
selectedRows.value.push(row);
|
||
}
|
||
emit('selection-change', selectedRows.value);
|
||
emit('select', row);
|
||
}
|
||
|
||
function isSelected(row) {
|
||
return selectedRows.value.some(r => r[props.rowKey] === row[props.rowKey]);
|
||
}
|
||
|
||
function toggleExpand(row) {
|
||
const key = row[props.rowKey];
|
||
const index = expandedKeys.value.indexOf(key);
|
||
if (index === -1) {
|
||
expandedKeys.value.push(key);
|
||
} else {
|
||
expandedKeys.value.splice(index, 1);
|
||
}
|
||
emit('expand-change', expandedKeys.value);
|
||
}
|
||
|
||
function isExpanded(row) {
|
||
return expandedKeys.value.includes(row[props.rowKey]);
|
||
}
|
||
|
||
return {
|
||
currentRow,
|
||
sortedData,
|
||
expandedKeys,
|
||
selectedRows,
|
||
sortConditions,
|
||
handleRowClick,
|
||
handleHeaderClick,
|
||
getSortIcon,
|
||
toggleSelect,
|
||
isSelected,
|
||
toggleExpand,
|
||
isExpanded
|
||
};
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.u-table2 {
|
||
width: auto;
|
||
overflow: auto;
|
||
white-space: nowrap;
|
||
|
||
.u-table-header {
|
||
min-width: 100% !important;
|
||
width: fit-content;
|
||
background-color: #f5f7fa;
|
||
}
|
||
|
||
.u-table-body {
|
||
min-width: 100% !important;
|
||
width: fit-content;
|
||
}
|
||
|
||
.u-table-sticky {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
}
|
||
|
||
.u-table-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
border-bottom: 1rpx solid #ebeef5;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.u-table-cell {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: row;
|
||
padding: 5px 4px;
|
||
font-size: 14px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.u-table-fixed-left {
|
||
position: sticky;
|
||
left: 0;
|
||
z-index: 9;
|
||
}
|
||
|
||
.u-table-fixed-right {
|
||
position: sticky;
|
||
right: 0;
|
||
z-index: 9;
|
||
}
|
||
|
||
.u-table-row-zebra {
|
||
background-color: #fafafa;
|
||
}
|
||
|
||
.u-table-row-highlight {
|
||
background-color: #f5f7fa;
|
||
}
|
||
|
||
.u-table-empty {
|
||
text-align: center;
|
||
padding: 20px;
|
||
color: #999;
|
||
}
|
||
|
||
.u-text-left {
|
||
text-align: left;
|
||
}
|
||
|
||
.u-text-center {
|
||
text-align: center;
|
||
}
|
||
|
||
.u-text-right {
|
||
text-align: right;
|
||
}
|
||
}
|
||
</style>
|