<template>
<view
class="lh-table"
:style="{
height: processedRows && processedRows.length ? props.height : 'auto',
}"
>
<scroll-view
v-if="processedRows.length"
ref="scrollView"
class="table"
scroll-x
scroll-y
:style="{ height: processedRows.length ? props.height : 'auto' }"
:scroll-into-view="scrollIntoView"
@scrolltolower="scroll"
>
<view class="scrollView">
<view class="table-info">
<view ref="scrollViewContent1" class="fixed-cloumn">
<view class="column">
<view
v-if="props.showChecker"
class="column"
style="width: 60rpx"
>
<view
:style="props.headerStyle"
class="column-title"
style="z-index: 10"
>
<checkbox
v-if="!props.radio && props.showCheckerAll"
@tap="checkAll"
:checked="state.checkAll"
:class="state.checkAllClass"
style="width: 34rpx; height: 34rpx"
></checkbox>
</view>
<view
v-for="(row, ind) in processedRows"
:key="ind"
class="column-row column-order"
:class="{ 'child-task-row': row.isChild }"
>
<checkbox
@tap="changeCheckbox(row, ind)"
:class="{ radio: props.radio }"
:checked="row[props.checkedStr]"
style="width: 34rpx; height: 34rpx"
></checkbox>
</view>
</view>
<view v-if="props.showIndex" class="column" style="width: 60rpx">
<view :style="props.headerStyle" class="column-title"
>序号</view
>
<view
@tap="tapRow(ind, row)"
v-for="(row, ind) in processedRows"
:key="ind"
class="column-row column-order"
:class="{ 'child-task-row': row.isChild }"
>
{{ ind + 1 }}
</view>
</view>
<!-- 固定列需指定宽度 -->
<view
v-for="(col, i) in props.columns.filter((col) => col.fixed)"
:key="i"
class="column fixedBox"
:style="{ width: col.width || '80rpx', '--column-width': col.width || '80rpx' }"
>
<view
:style="props.headerStyle"
class="column-title"
@tap.native="onFixedHeaderChange(col)"
:class="{
'title-left': col.align === 'left',
'title-right': col.align === 'right',
'title-center': col.align === 'center' || !col.align
}"
>{{ col.title }}</view
>
<view
v-for="(row, ind) in processedRows"
:key="ind"
class="column-row"
:class="{ 'child-task-row': row.isChild }"
:style="{
textAlign: col.align || 'center',
justifyContent: getJustifyContent(col.align)
}"
>
<slot :row="row" :name="col.prop" :rowIndex="ind">
{{ row[col.prop] }}
</slot>
</view>
</view>
</view>
</view>
<view ref="scrollViewContent2" class="arrange-column">
<view class="arrange-column-box">
<view
v-for="(col, index) in props.columns.filter(
(col) => !col.fixed
)"
:key="index"
class="column"
:class="{ 'has-child': col && col.hasChild }"
:id="'column' + index"
:scrollIntoView="scrollIntoView"
:style="{ '--column-width': col.width || 'auto' }"
>
<view>
<view
:style="props.headerStyle"
class="column-title"
@tap.native="onSortChange(col, index)"
:class="{
'title-left': col.align === 'left',
'title-right': col.align === 'right',
'title-center': col.align === 'center' || !col.align
}"
>
{{ col.title }}
<view class="arrow-box" v-if="props.isSort">
<text
class="arrow up"
:class="{
active:
sortMode === 1 && clickedHeaderIndex === index,
}"
></text>
<text
class="arrow down"
:class="{
active:
sortMode === 2 && clickedHeaderIndex === index,
}"
></text>
</view>
</view>
<view v-if="processedRows.length">
<view
v-for="(row, ind) in processedRows"
:key="ind"
class="column-row"
:class="{ 'child-task-row': row.isChild }"
:style="{
textAlign: col.align || 'center',
whiteSpace: col.width ? 'normal' : 'nowrap',
justifyContent: getJustifyContent(col.align)
}"
>
<view v-if="col.slot">
<slot
:row="row"
:name="col.prop"
:rowIndex="ind"
></slot>
</view>
<view v-else-if="col.formate">
{{ col.formate(row[col.prop], row, ind) }}
</view>
<view v-else>
<uni-tooltip
v-if="col.width"
:content="row[col.prop]"
placement="bottom"
>
<view
:style="{ width: col.width || 'auto' }"
class="tooltip"
>{{ row[col.prop] }}</view
>
</uni-tooltip>
<text v-else>
{{ row[col.prop] }}
</text>
</view>
</view>
</view>
</view>
</view>
<view v-if="props.showOper" class="oper-column">
<view class="column">
<view :style="props.headerStyle" class="column-title"
>操作</view
>
<view
v-for="(row, ind) in processedRows"
:key="ind"
class="column-row"
:class="{ 'child-task-row': row.isChild }"
>
<slot :row="row" :rowIndex="ind" name="operCol"></slot>
</view>
</view>
</view>
</view>
</view>
</view>
<uni-load-more
v-if="processedRows.length"
:status="props.loading ? 'loading' : 'noMore'"
:content-text="{
contentdown: '加载中...',
contentrefresh: '加载中...',
contentnomore: '没有更多了',
}"
></uni-load-more>
</view>
</scroll-view>
<uni-load-more
v-else
:status="props.loading ? 'loading' : 'noMore'"
:content-text="{
contentdown: '加载中...',
contentrefresh: '加载中...',
contentnomore: '暂无数据',
}"
></uni-load-more>
<view v-if="props.showFooter" class="footer">
<slot name="footer">
<view>计划放点击翻页</view>
</slot>
</view>
</view>
</template>
<script setup>
import {
ref,
reactive,
getCurrentInstance,
onMounted,
watch,
nextTick,
computed,
} from "vue";
const instance = getCurrentInstance();
const emits = defineEmits([
"tapOper",
"tabRow",
"loadMore",
"changeRadiobox",
"changeCheckbox",
"checkAll",
"refresh",
"cancelCheckbox",
]);
const props = defineProps({
// 判断是否显示序号
showIndex: {
type: Boolean,
default: false,
},
// 用于控制是否显示页脚。默认为false
showFooter: {
type: Boolean,
default: false,
},
// 用于控制是否显示操作按钮
showOper: {
type: Boolean,
default: false,
},
// 用于控制是否显示复选框等
showChecker: {
type: Boolean,
default: false,
},
// 是否展示多选全选按钮
showCheckerAll: {
type: Boolean,
default: true,
},
// 接口判断选中的字段名
checkedStr: {
type: String,
default: "selected",
},
// 默认多选,打开单选
radio: {
type: Boolean,
default: false,
},
// 判断是否显示序号
showIndex: {
type: Boolean,
default: false,
},
// 用于定义表格的列信息。默认为空数组
columns: {
type: Array,
default: () => [],
},
// 用于定义表格的行数据。默认为空数组,初始20条
rows: {
type: Array,
default: () => [],
},
// 用于定义表格头部的样式
headerStyle: {
type: String,
default: "",
},
// 用于定义表格或组件的高度
height: {
type: String,
default: "",
},
// 是否显示加载状态
loading: {
type: Boolean,
default: false,
},
//滚动到的指定位置
scrollIntoView: {
type: String,
default: "column0",
},
// 是否排序
isSort: {
type: Boolean,
default: false,
},
// 是否显示子项
showChildTasks: {
type: Boolean,
default: true,
},
// 子项字段配置
childConfig: {
type: Object,
default: () => ({
childListField: "childList", // 子项列表字段名
childCountField: "childCount", // 子项数量字段名
parentIdField: "id", // 父项ID字段名
}),
},
});
const clickedHeaderIndex = ref(0); //排序下标
const clickedHeaderCode = ref(""); //排序对应的表头code
const sortMode = ref("");
const onSortChange = (column, index) => {
clickedHeaderIndex.value = index;
// 展示下标 上箭头/下箭头
sortMode.value = Number(sortMode.value) + 1;
sortMode.value = sortMode.value < 3 ? sortMode.value : "";
emits("onSortChange", { column, sortMode: sortMode.value, index });
};
// 获取选中的数据 改为 表格全选图标回显
const getCheckList = (array, checkedLabel, key = "id") => {
let checkIdList = array.filter((obj) => obj[checkedLabel]);
let checkIdListMap = checkIdList.map((v) => v[key]);
if (checkIdListMap.length) {
// processedRows.value.forEach((obj) => {
// if (checkIdListMap.includes(obj[key])) {
// Object.assign(obj, { checked: true });
// }
// });
const allCountLength = array.length;
if (checkIdList.length == allCountLength) {
state.checkAllClass = "";
state.checkAll = true;
}
if (checkIdList.length < allCountLength) {
state.checkAll = false;
state.checkAllClass = "variable";
}
} else {
state.checkAllClass = "";
state.checkAll = false;
}
};
defineExpose({ getCheckList, onSortChange }); //抛出才去得到
const arrangeColumnListWidth = ref(0);
const fixedColumnsArray = props.columns.filter((col) => col.fixed);
const arrangeColumnWidth = ref(0);
const extractNumberFromString = (input) => {
// 使用正则表达式匹配数字部分
const match = input.match(/\d+/);
// 如果匹配成功,则返回匹配到的数字,否则返回null
return match ? parseInt(match[0], 10) : null;
};
const state = reactive({
checkAll: false,
checkAllClass: "",
height: `calc(${props.height} ? '60px' : '1px') - ${
props.showFooter ? "80px" : "1px"
}`,
});
// 纵向滚动条事件
const scroll = (e) => {
// console.log('开始gun')
if (e.detail.direction === "bottom") {
emits("loadMore");
console.log("滑动到底部");
return;
}
};
// 点击单元格事件
const tapRow = (ind, row) => {
emits("tapRow", {
index: ind,
data: row,
});
};
const onFixedHeaderChange = (column) => {
emits("onFixedHeaderChange", { column });
};
// 根据对齐方式获取justify-content值
const getJustifyContent = (align) => {
switch (align) {
case 'left':
return 'flex-start';
case 'right':
return 'flex-end';
case 'center':
default:
return 'center';
}
};
// 全选
const checkAll = () => {
state.checkAllClass = "";
state.checkAll = !state.checkAll;
props.rows.forEach((row) => {
row[props.checkedStr] = state.checkAll;
});
emits("checkAll", { data: processedRows.value, selected: state.checkAll });
};
// 选中
const changeCheckbox = (row, ind) => {
row[props.checkedStr] = !row[props.checkedStr];
// 单选
if (props.radio) {
props.rows.forEach((v, i) => {
v[props.checkedStr] = ind === i;
});
emits("changeRadiobox", { index: ind, data: row });
} else {
// 多选
const allCountLength = processedRows.value.length;
const checkedCount = processedRows.value.filter(
(row) => row[props.checkedStr]
);
const checkedCountLength = checkedCount.length;
if (allCountLength == checkedCountLength) {
state.checkAllClass = "";
state.checkAll = true;
}
if (!checkedCountLength) {
state.checkAllClass = "";
state.checkAll = false;
}
if (checkedCountLength && checkedCountLength < allCountLength) {
state.checkAll = false;
state.checkAllClass = "variable";
}
if (!row[props.checkedStr]) {
//取消选中
emits("cancelCheckbox", row);
}
emits("changeCheckbox", checkedCount);
}
};
// 处理子项数据的计算属性
const processedRows = computed(() => {
if (!props.showChildTasks || !Array.isArray(props.rows)) {
return props.rows || [];
}
const result = [];
const { childListField, childCountField, parentIdField } = props.childConfig;
props.rows.forEach((item) => {
// 添加父任务
result.push({
...item,
hasChild: (item[childCountField] || 0) > 0,
isChild: false,
_raw: item,
});
// 如果有子任务且显示子任务,添加子任务
if (
item[childListField] &&
Array.isArray(item[childListField]) &&
item[childListField].length > 0
) {
item[childListField].forEach((child) => {
result.push({
...child,
hasChild: (child[childCountField] || 0) > 0,
isChild: true,
parentId: item[parentIdField],
_raw: child,
});
});
}
});
getCheckList(props.rows, props.checkedStr);
return result;
});
</script>
<style>
</style>
<style lang="scss" scoped>
::v-deep .uni-scroll-view-content {
display: inline-flex;
}
.lh-table {
flex: 1;
width: 100%;
overflow: hidden;
}
.fixed-cloumn {
position: sticky;
left: -3rpx;
z-index: 10;
display: inline-block;
background: #fff;
border-left: 2rpx solid #fff;
border-right: 2rpx solid #fff;
.fixedBox {
background: #fff;
z-index: 99;
width: var(--column-width, auto);
box-sizing: border-box;
}
.column-title {
z-index: 99;
background: linear-gradient(#ececfc, #ffffff) !important;
border-top: 2rpx solid #ffffff;
border-bottom: none !important;
width: 100%;
box-sizing: border-box;
}
.column-row:nth-child(2n) {
background-color: #fff !important;
}
.column-row:nth-child(2n-1) {
background: rgba(190, 190, 190, 0.2);
}
}
.oper-column {
position: sticky;
right: 0;
z-index: 10;
display: inline-block;
background: #fff;
.column {
border: 2rpx solid #fff !important;
border-top: none !important;
}
.column-title {
z-index: 99;
background: linear-gradient(#ececfc, #ffffff) !important;
border-top: 2rpx solid #ffffff;
border-bottom: none !important;
}
.column-row:nth-child(2n) {
background: #fff !important;
}
.column-row:nth-child(2n-1) {
background: rgba(190, 190, 190, 0.2);
}
}
.footer {
height: 80px;
padding: 16rpx;
font-size: 18px;
}
.table {
width: 100%;
box-sizing: border-box;
white-space: nowrap;
display: flex;
padding: 13rpx;
.table-info {
display: flex;
flex: 1;
}
.arrange-column {
flex: 1;
display: inline-block;
border-right: 2rpx solid #fff;
.arrange-column-box {
display: flex;
flex: 1;
}
}
.column {
display: inline-block;
// border-left: 1px solid #eee;
flex-grow: 1;
flex-shrink: 0;
width: var(--column-width, auto);
background: #fff;
.column-row:nth-child(2n) {
background: rgba(190, 190, 190, 0.2);
}
}
.column:first-child {
border-left: none;
}
.column.has-child {
flex: none;
flex-shrink: 0;
}
.oper:nth-child(n + 2) {
margin-left: 16rpx;
}
}
.column-row {
text-align: center;
padding: 10rpx;
box-sizing: border-box;
text-overflow: ellipsis;
min-width: 60rpx;
font-size: 24rpx;
color: #000000;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: nowrap;
min-height: 94rpx;
background-color: #fff;
border-bottom: 2rpx solid rgba(123, 123, 123, 0.2);
overflow: hidden;
width: 100%;
word-break: break-all;
white-space: nowrap;
}
.column-title {
height: 64rpx !important;
line-height: 40rpx;
font-weight: 500;
color: #868686;
position: sticky;
position: -webkit-sticky;
top: -2rpx !important;
font-size: 24rpx;
padding: 10rpx;
z-index: 9;
background: linear-gradient(#ececfc, #ffffff);
border-top: 2rpx solid #ffffff;
border-bottom: none !important;
text-align: center;
overflow: hidden;
width: 100%;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
word-break: break-all;
white-space: nowrap;
&.title-left {
justify-content: flex-start;
text-align: left;
}
&.title-right {
justify-content: flex-end;
text-align: right;
}
&.title-center {
justify-content: center;
text-align: center;
}
}
::v-deep .uni-checkbox-input {
width: 32rpx;
height: 32rpx;
box-sizing: border-box;
border: 1px solid #7b7b7b;
background-color: transparent;
}
::v-deep .uni-checkbox-input svg {
color: #e70c0c;
path {
fill: #e70c0c !important;
}
}
.radio {
::v-deep .uni-checkbox-input {
border-radius: 50%;
}
}
::v-deep .variable .uni-checkbox-input::after {
height: 30rpx;
content: "-";
font-size: 28px;
line-height: 20rpx;
color: #e70c0c;
display: inline-block;
}
.no_more {
text-align: center;
font-size: 24rpx;
font-weight: 500;
color: #868686;
padding: 0 40rpx;
}
.loading-state {
text-align: center;
padding: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.loading-spinner {
width: 40rpx;
height: 40rpx;
border: 4rpx solid #f3f3f3;
border-top: 4rpx solid #e70c0c;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16rpx;
}
text {
font-size: 24rpx;
color: #868686;
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.state-more {
padding: 10rpx;
flex-direction: row;
font-size: 24rpx;
color: #868686;
text-align: center;
.loading-spinner {
width: 25rpx;
height: 25rpx;
margin-bottom: 0rpx;
margin-right: 16rpx;
}
}
// 排序图标
.arrow-box {
position: relative;
bottom: 0rpx;
right: 10rpx;
top:4rpx;
display: inline-block;
}
.arrow {
display: block;
position: relative;
width: 10px;
height: 8px;
// border: 1px red solid;
left: 5px;
overflow: hidden;
cursor: pointer;
}
.down {
top: 3px;
}
.down::after {
content: "";
width: 8px;
height: 8px;
position: absolute;
left: 2px;
top: -5px;
transform: rotate(45deg);
background-color: #ccc;
}
.down.active::after {
background-color: #e70c0c;
}
.up::after {
content: "";
width: 8px;
height: 8px;
position: absolute;
left: 2px;
top: 5px;
transform: rotate(45deg);
background-color: #ccc;
}
.up.active::after {
background-color: #e70c0c;
}
.scrollView {
width: 100%;
display: inline-table;
box-sizing: border-box;
}
// 展示省略号
.tooltip {
white-space: nowrap; /* 禁止换行 */
overflow: hidden; /* 隐藏溢出内容 */
text-overflow: ellipsis;
}
</style>