使用案例
<Table :columns="columns" :data="data" headerBorder />
Table父组件
<template>
<div class="table-container" v-bind="$attrs">
<TableHeader
:columns="columns"
:headerBG="headerBG"
:getColumnWidth="getColumnWidth"
:headerBorder="headerBorder"
@sort="handleSort"
/>
<div
class="table-body"
:style="{
overflowX: scroll.x ? 'auto' : 'hidden',
overflowY: scroll.y ? 'auto' : 'hidden',
maxHeight: scroll.y ? scroll.y : 'none',
maxWidth: scroll.x ? scroll.x : 'none'
}"
>
<div class="emity" v-if="!data || data?.length == 0">
<a-empty :image="simpleImage">
<template #description>
<span style="color: #adafb0"> 暂无数据 </span>
</template>
</a-empty>
</div>
<div
v-else
class="table-row"
v-for="(row, index) in sortedData"
:key="row.key"
@click.stop="onClickRow(row)"
>
<TableCell
v-for="column in columns"
:key="column.key"
:column="column"
:row="row"
:index="index"
:getColumnWidth="getColumnWidth"
>
<template
v-slot:[column.dataIndex]="slotProps"
@click.stop.self="(e) => emits('click', e)"
>
<slot :name="column.dataIndex" v-bind="slotProps" />
</template>
</TableCell>
</div>
</div>
</div>
</template>
<script setup lang="js">
import { ref, computed, defineProps } from 'vue';
import TableHeader from './TableHeader.vue';
import TableCell from './TableCell.vue';
import { Empty } from 'ant-design-vue';
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE;
const props = defineProps({
columns: {
type: Array,
required: true
},
headerBorder: {
type: Boolean,
default: false
},
headerBG: {
type: Boolean,
default: false
},
data: {
type: Array,
required: true
},
scroll: {
type: Object,
default: () => ({
x: '100%',
y: '300px'
})
}
});
const emits = defineEmits(['onClickRow', 'click', 'sort']);
const initData = computed(() => {
const arr = props.data.map((val, index) => {
console.log(index, 'index');
return {
...val,
index: index + 1
};
});
return arr;
});
const init = () => {
return JSON.parse(JSON.stringify(initData.value));
};
const sortedData = ref(init());
const totalFixedWidth = computed(() => {
return props.columns.reduce((acc, column) => {
return acc + (column.width ? parseFloat(column.width) : 0);
}, 0);
});
const dynamicColumnCount = computed(() => {
return props.columns.filter((column) => !column.width).length;
});
const dynamicColumnWidth = computed(() => {
return dynamicColumnCount.value > 0
? `${(100 - totalFixedWidth.value) / dynamicColumnCount.value}%`
: '0%';
});
const handleSort = (column) => {
const { dataIndex, sort, sorter } = column;
const sortOrder = sort === 'asc' ? 1 : -1;
const compare = (a, b) => {
if (column.type === 'index') {
return a.index - b.index;
} else {
if (a[dataIndex] === b[dataIndex]) {
return 0;
}
return a[dataIndex] > b[dataIndex] ? 1 : -1;
}
};
if (!sorter) {
sortedData.value.sort((a, b) => compare(a, b) * sortOrder);
} else {
sortedData.value.sort((a, b) => sorter.compare(a, b) * sortOrder);
}
column.sort = sort === 'asc' ? 'desc' : sort === 'desc' ? null : 'asc';
if (column.sort === null) {
sortedData.value = [...init()];
}
};
const onClickRow = (row) => {
emits('onClickRow', row);
};
const getColumnWidth = (column) => {
return column.width ? column.width : dynamicColumnWidth.value;
};
</script>
<style lang="less" scoped>
.table-container {
display: flex;
flex-direction: column;
font-size: 16px;
width: 100%;
font-family: PingFang SC;
font-weight: normal;
line-height: normal;
letter-spacing: 0em;
font-variation-settings: 'opsz' auto;
color: #ffffff;
}
.table-header {
border-radius: 2px;
opacity: 1;
padding: 8px 0px;
display: flex;
}
.table-row {
width: 100%;
display: flex;
justify-content: space-between;
height: 40px;
background: linear-gradient(
270deg,
rgba(44, 174, 255, 0.05) 0%,
rgba(44, 174, 255, 0.1) 100%
);
box-sizing: border-box;
border: 0.6px solid;
border-image: linear-gradient(
270deg,
rgba(44, 174, 255, 0.1) 0%,
rgba(44, 174, 255, 0.225) 84%,
rgba(44, 174, 255, 0.4) 100%
)
0.6;
margin-bottom: 5px;
}
.table-row:hover {
border-radius: 2px;
box-sizing: border-box;
background: linear-gradient(
0deg,
rgba(44, 174, 255, 0.13),
rgba(44, 174, 255, 0.13)
),
linear-gradient(
270deg,
rgba(25, 82, 93, 0) 0%,
rgba(55, 136, 187, 0.4) 100%,
#19435d 100%
);
box-sizing: border-box;
border: 1px solid;
border-image: linear-gradient(
270deg,
rgba(44, 174, 255, 0.57) 0%,
#2caeff 47%,
#7fcbfa 100%
)
1;
box-shadow: inset 0px 0px 6px 0px rgba(78, 187, 255, 0.5);
}
.table-cell {
padding: 5px 6px;
text-align: left;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
flex-shrink: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
* {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.table-body::-webkit-scrollbar {
display: none;
}
.table-cell-children {
display: flex;
justify-content: space-around;
}
.table-cell button {
padding: 4px 8px;
cursor: pointer;
}
</style>
TableCell渲染表格行子组件
<template>
<div
class="table-cell"
:style="{
...column.dataStyle,
width: getColumnWidth(column),
textAlign: column.align || 'center'
}"
:title="
Array.isArray(row[column.dataIndex])
? !column.slot
? row[column.dataIndex].join(', ')
: null
: !column.slot
? row[column.dataIndex]
: null
"
>
<div v-if="column?.children" class="table-cell-children">
<TableCell
v-for="item in column?.children"
:key="item?.key"
:column="item"
:row="row"
:index="index"
:getColumnWidth="getColumnWidth"
>
<template v-slot:[item.dataIndex]="slotProps">
<slot :name="item.dataIndex" v-bind="slotProps"> </slot>
</template>
</TableCell>
</div>
<span v-if="column.slot" :title="row[column.dataIndex]">
<slot
:name="column.dataIndex"
:record="row"
:index="index"
:text="row[column.dataIndex]"
:column="column"
>
</slot>
</span>
<template v-else>
<span class="sortImg" v-if="column.type === 'index'">
{{ row.index }}
</span>
<span v-else>
{{
Array.isArray(row[column.dataIndex])
? row[column.dataIndex].join(', ')
: row[column.dataIndex]
}}
</span>
</template>
</div>
</template>
<script setup>
const props = defineProps({
column: {
type: Object,
required: true
},
row: {
type: Object,
required: true
},
index: {
type: Number,
required: true
},
getColumnWidth: {
type: Function,
required: true
}
});
</script>
<style lang="less" scoped>
.sortImg {
background: url(@/assets/common/sort.png) no-repeat;
background-size: 100% 100%;
text-align: center;
width: 28px;
height: 28px;
line-height: 28px;
}
.table-cell {
font-family: PingFang SC;
font-size: 16px;
font-weight: normal;
line-height: 100%;
letter-spacing: 0em;
font-variation-settings: 'opsz' auto;
color: #ffffff;
align-items: center;
}
.table-cell button {
padding: 4px 8px;
cursor: pointer;
}
</style>
TableHeader渲染表格头部子组件
<template>
<div class="table-header" :class="headerBG ? 'table-header-img' : ''">
<div
class="table-cell"
v-for="column in columns"
:key="column?.key"
:style="{
border: headerBorder ? '1px solid rgba(255, 255, 255, 0.13)' : '',
...column.headerStyle,
width: getColumnWidth(column),
textAlign: column?.align || 'center'
}"
@click.stop="handleSort(column)"
:title="column.title"
>
<div class="flex items-center justify-center gap-[2px]">
<span>{{ column.title }} </span>
<div class="flex flex-col items-center" v-show="column?.sortable">
<CaretUpOutlined
:style="{
color:
column.sort === 'asc'
? '#3b9eedd6'
: 'rgba(255, 255, 255, 0.4)',
fontSize: '12px',
height: '8px'
}"
/>
<CaretDownOutlined
:style="{
color:
column.sort === 'desc'
? '#3b9eedd6'
: 'rgba(255, 255, 255, 0.4)',
fontSize: '12px',
height: '8px'
}"
/>
</div>
</div>
<div v-if="column?.children" class="table-cell-children">
<TableHeader
:headerBorder="headerBorder"
:columns="column.children"
:getColumnWidth="getColumnWidth"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, defineProps } from 'vue';
import { CaretUpOutlined, CaretDownOutlined } from '@ant-design/icons-vue';
const props = defineProps({
columns: {
type: Array,
required: true
},
headerBorder: {
type: Boolean,
default: false
},
headerBG: {
type: Boolean,
default: false
},
getColumnWidth: {
type: Function,
required: true
}
});
const columns = ref(props.columns);
columns.value.forEach((column) => {
if (column.sortable) {
column.sort = null;
}
});
const emit = defineEmits(['sort']);
const handleSort = (column) => {
if (column.sortable) {
if (column.sort === 'asc') {
column.sort = 'desc';
} else if (column.sort === 'desc') {
column.sort = null;
} else {
column.sort = 'asc';
}
emit('sort', column);
}
};
</script>
<style lang="less" scoped>
.table-header-img {
background-image: url(@/assets/common/headBg.png) !important;
background-size: 100% 100% !important;
}
.table-cell {
font-family: PingFang SC;
font-size: 14px;
font-weight: normal;
line-height: normal;
letter-spacing: 0em;
cursor: pointer;
color: rgba(255, 255, 255, 0.8);
}
</style>