YingXingAI/node_modules/uview-plus/components/u-table2/u-table2.vue

518 lines
16 KiB
Vue
Raw Normal View History

2025-07-09 16:43:03 +08:00
<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>