uni app 表格封装

<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>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值