手写vue3版本antUI组件库表格满足部分api功能

使用案例

<Table :columns="columns" :data="data" headerBorder />
//支持部分ant的columns属性配置
/**
 *    <Table :columns="columns" :data="data" headerBorder />
 *  Table:{
 *   headerBorder:开启表格头部边框 ,
 *  column:表格配置
 *   data:表格数据
 * }
 *
 * Column {
 *  title: 'Name',
    dataIndex: 'name',
 *  align: 'left'
     type: 'index', =>开启序号
    wdth :单位百分比
    slot:true, => :具名插槽="column.dataIndex"
    sortable: true, =>开启排序 
    sorter: { //自定义排序方法
      compare: (a, b) => a - b
     }
   dataStyle:{   marginTop: '5px'}     数据样式

     headerStyle: {
          marginTop: '5px'     头样式
        },
 * }

 开启分组配置
   {
    title: '营收',
    align: 'center',
    width: '20%',
    headerStyle: {
      padding: 'unset',
      paddingTop: '10px'
    },
    children: [
      {
        title: '绝对值(亿元)',
        dataIndex: 'age',
        key: 'age',
        width: '50%',
        headerStyle: {
          marginTop: '5px'
        },
        align: 'center'
      },
      {
        title: '增速(%)',
        dataIndex: 'address',
        key: 'address',
        width: '50%',
        headerStyle: {
          marginTop: '5px'
        },
        align: 'center'
      }
    ]
  },
 */

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;
//支持部分ant的columns属性配置
/**
 *    <Table :columns="columns" :data="data" headerBorder />
 *  Table:{
 *   headerBorder:开启表格头部边框 ,
 *  column:表格配置
 *   data:表格数据
 * }
 *
 * Column {
 *  title: 'Name',
    dataIndex: 'name',
 *  align: 'left'
     type: 'index', =>开启序号
    wdth :单位百分比
    slot:true, => :具名插槽="column.dataIndex"
    sortable: true, =>开启排序 
    sorter: { //自定义排序方法
      compare: (a, b) => a - b
     }
   dataStyle:{   marginTop: '5px'}     数据样式

     headerStyle: {
          marginTop: '5px'     头样式
        },
 * }

 开启分组配置
   {
    title: '营收',
    align: 'center',
    width: '20%',
    headerStyle: {
      padding: 'unset',
      paddingTop: '10px'
    },
    children: [
      {
        title: '绝对值(亿元)',
        dataIndex: 'age',
        key: 'age',
        width: '50%',
        headerStyle: {
          marginTop: '5px'
        },
        align: 'center'
      },
      {
        title: '增速(%)',
        dataIndex: 'address',
        key: 'address',
        width: '50%',
        headerStyle: {
          marginTop: '5px'
        },
        align: 'center'
      }
    ]
  },
 */
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%', // 可以设置为像素值、百分比或true
      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;

  // 根据 sort 状态确定排序方向
  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';

  // 如果排序状态为 null,则重置为原始数据顺序
  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;
  // display: flex;
  // align-items: center;
  // justify-content: center;
}
.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; // 'none', 'asc', 'desc'
  }
});
const emit = defineEmits(['sort']);
// desc asc
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);
}
// .table-cell button {
//   padding: 4px 8px;
//   cursor: pointer;
// }
</style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值