基于antdv封装的特殊复杂表格,带通行描述信息
主要功能:
- 可展示通行的单元格信息
- 可跨页选择数据功能
- 表单插槽、合计插槽、操作按钮区插槽
- 分页功能
- 接口内请求api
- 可编辑单元格
- 表格组件暴漏出的方法:查询、获取选中数据、接口返回数据、当前表格数据【设置可编辑单元格时获取数据】
页面示例:
本页主要是下面这种大通行效果【选择和序号都通行】
还有一种是通行不带选择和序号,如下:代码下载地址 : https://download.csdn.net/download/qq_32442967/87965962
使用示例:
页面文件page.vue
<template>
<self-table
ref="selfTableRef"
:full-row-type="fullRowType"
:api="planManageListApi"
:columns="tableColumns(fullRowType)"
:full-row-key-list="['noId']"
:has-checkbox="true"
:allTableColumnsNum="8"
:tableProps="{}"
>
<template #form>
<!-- 自定义表单组件 可用任意表单组件 submit为表单 查询按钮事件 -->
<BasicForm @submit="handleSubmit" />
</template>
<template #tableTitle> 订单总重量:[<span class="color-red">1244.533</span> 吨] </template>
<template #tableHead>
<a-button type="primary">导出</a-button>
</template>
<template #full-row="{ row }">
计划单号:<a-button type="link" size="small" @click="handlePlanDetail(row)">{{ row.noId }}</a-button> 计划总量:1 吨申报时间:2023-04-26
17:09:26 审核时间:2023-04-26 17:09:46
</template>
<template #colSlot1="{ row }">
<div>申报单位:{{ row.xa }}XXXXX有限责任公司</div>
<div>申报人:欧冶材料</div>
</template>
<template #colSlot4="{ row }">
<div>钢材 10 {{ row.xs }}</div>
<div>钢材 20</div>
</template>
<template #action="{ row }">
<a-button type="link" size="small" @click="handlePlanApply('edit', row)">修改</a-button>
</template>
</self-table>
</template>
<script setup lang="ts">
import SelfTable from '/@/views/components/SelfTable/index.vue';
import { ref } from 'vue';
import { FullRowType } from '/@/views/components/SelfTable/selfTableTools';
import { tableColumns } from '/@/views/planManage/planManage.data';
import { planManageListApi } from '/@/views/planManage/planManage.api';
// 定义通行类型
const fullRowType: FullRowType = 'prev';
const selfTableRef = ref();
function handleSubmit(v) {
selfTableRef.value.search(v);
}
</script>
<style scoped lang="less"></style>
planManage.data中tableColumns() 方法
export const tableColumns = (fullRowType): SelfTableColumnType[] => [
{
title: '计划信息',
dataIndex: 'key1',
width: 300,
customCell: (_, index) => sharedOnCell(index, fullRowType),
slot: 'colSlot1',
},
{
title: '计划状态',
dataIndex: 'planStatus',
width: 120,
customCell: (_, index) => sharedOnCell(index, fullRowType),
},
{
title: '区域',
dataIndex: 'oldRecordCdName',
width: 220,
align: 'center',
customCell: (_, index) => sharedOnCell(index, fullRowType),
},
{
title: '详情',
dataIndex: 'createTime',
width: 300,
customCell: (_, index) => sharedOnCell(index, fullRowType),
slot: 'colSlot4',
},
{
title: '审核人',
dataIndex: 'accountMark',
width: 100,
align: 'center',
customCell: (_, index) => sharedOnCell(index, fullRowType),
},
{
title: '操作',
key: 'action',
fixed: 'right',
align: 'center',
width: 180,
customCell: (_, index) => sharedOnCell(index, fullRowType),
},
];
参数说明:
字段 | 类型 | 描述 |
---|---|---|
api | Promise | 表格数据api |
dataSource | array | 表格数据 |
columns | array | 表格column数据,列数据一定要指定width |
has-checkbox | boolean | 是否可选,默认false |
full-row-type | string | 通行类型 ‘prev’ 前一行,‘last’ 后一行 ,FullRowType类型 |
tableProps | Object | 传给antd table组件的数据 |
allTableColumnsNum | number | 所有的表格列数【包含选择列和序号列】 |
SelfTable.vue组件
<template>
<a-card style="margin: 8px" v-if="$slots['form']">
<slot name="form"></slot>
</a-card>
<component :is="isDiv ? 'div' : 'a-card'" style="margin: 8px">
<!-- 可放操作按钮 -->
<div class="table-head" v-if="$slots['tableHead']">
<slot name="tableHead"></slot>
</div>
<!-- 可放合计数据 -->
<div class="table-head" v-if="$slots['tableTitle']">
<slot name="tableTitle"></slot>
</div>
<a-alert class="a-alert-cont" type="info" show-icon v-if="hasCheckbox">
<template #message>
<span>{{ checkedAlertMessage }}</span>
<template v-if="checkedList.length">
<a-divider type="vertical" />
<a href="javascript:" @click="resetCheckbox">清空</a>
</template>
</template>
</a-alert>
<a-table
:columns="columns"
:data-source="tableData"
:scroll="{ x: '100%' }"
:pagination="false"
size="small"
@change="pageChange"
bordered
v-bind="$attrs.tableProps"
class="self-table"
>
<!-- 编辑列显示编辑ICON -->
<template #headerCell="{ column, title }">
<template v-if="column.isEdit === true">
<div class="head-edit-cont"> <i class="vxe-cell--edit-icon vxe-icon--edit-outline"></i> {{ title }} </div>
</template>
<!-- 选择 -->
<template v-if="column.dataIndex === 'rowSelection'">
<a-checkbox :indeterminate="indeterminate" v-model:checked="checkAll" @change="onCheckAllChange" />
</template>
</template>
<template #bodyCell="{ column, index, record, text }">
<!-- 序号 非通行时显示序号 -->
<template v-if="index % 2 === unFullRowIndex && column.dataIndex === 'selfIndex'">
{{ tableIndex(index) }}
</template>
<!--如果是通行则展示full-row插槽,如果不是通行并且传入了插槽则展示 -->
<template v-if="column.fullFirst">
<div class="full-row-cont" v-if="index % 2 === fullRowIndex">
<slot name="full-row" :row="record"></slot>
</div>
<!-- 选择列 -->
<template v-else-if="column.dataIndex === 'rowSelection'">
<a-checkbox
name="tableCheckbox"
class="my-checkbox"
v-model:checked="record._checked"
:key="record.id"
:value="record.id"
@change="onCheckBoxChange(record)"
/>
</template>
<template v-else-if="column.slot">
<slot :name="column.slot" :row="record" :rowIndex="index"></slot>
</template>
<template v-else>{{ text }}</template>
</template>
<!-- 如果不是通行、并且传入了插槽,则展示 -->
<template v-else-if="index % 2 === unFullRowIndex && column.slot">
<slot :name="column.slot" :row="record" :rowIndex="index"></slot>
</template>
<!-- 操作 -->
<template v-if="column.key === 'action'">
<template v-if="index % 2 === unFullRowIndex">
<slot name="action" :row="record"></slot>
</template>
</template>
</template>
</a-table>
<!-- 页码:传入dataSource时不显示页码 -->
<div class="my-footer" v-if="hasPagination">
<a-pagination
size="small"
:total="total"
:current="pageNo"
:page-size="pageSize"
show-size-changer
show-quick-jumper
:show-total="(total) => `共 ${total} 条数据`"
@change="pageChange"
/>
</div>
</component>
</template>
<script lang="ts" setup name="SelfTable">
import type { TableColumnType } from 'ant-design-vue';
import {
checkColumnsHasExist,
filterSelfTableColumns,
filterSelfTableData,
FullRowType,
getIndeterminateAndCheckAllStatus,
getAllCheckedListData,
useFullRowIndex,
SelfTableColumnType,
columnColspanFull,
indexColumnColspan,
} from '/@/views/components/SelfTable/selfTableTools';
import { computed, onMounted, reactive, ref, toRaw, watch } from 'vue';
interface Props {
dataSource?: any[]; // 数据,
fullRowType: FullRowType; // 同行类型
fullRowKeyList: any[]; // 展示在同行的数据,组件内自动转换数据
api?: any;
hasCheckbox?: boolean;
hasPagination?: boolean;
columns: TableColumnType[];
allTableColumnsNum: number; // 表格列总数量
isDiv?: boolean;
}
const props = defineProps<Props>();
// fullRowType 通行索引 0代表偶数行为通行,即行下展示描述信息
const { fullRowIndex, unFullRowIndex } = useFullRowIndex(props.fullRowType);
// 表格基础数据
let pageNo = ref(1); // 页码
let pageSize = ref(10); // 页条数
let total = ref(0); // 总条数
let sourceData = ref([]); // 接口返回原始数据
let tableSourceData = ref([]); // 接口返回原始数据中的表格数据
let tableData = ref([]); // 表格数据
const hasPagination = ref(true); // 是否显示页码
const checkColumn: SelfTableColumnType = {
title: ' ',
dataIndex: 'rowSelection',
key: Math.random(),
fixed: 'left',
align: 'center',
width: 40,
customCell: (_, index) => ({
colSpan: columnColspanFull(index, fullRowIndex, props.allTableColumnsNum),
}),
fullFirst: props.hasCheckbox,
};
const indexColumn: SelfTableColumnType = {
title: '序号',
key: Math.random(),
dataIndex: 'selfIndex',
fixed: 'left',
align: 'center',
width: 40,
customCell: (_, index) => ({
colSpan: indexColumnColspan(index, fullRowIndex, unFullRowIndex, props.allTableColumnsNum, props.hasCheckbox),
}),
fullFirst: !props.hasCheckbox,
};
let columns = ref<TableColumnType[]>([]);
columns.value = filterSelfTableColumns(toRaw(props.columns));
!checkColumnsHasExist(props.columns, 'selfIndex') && columns.value.unshift(indexColumn);
props.hasCheckbox && !checkColumnsHasExist(props.columns, 'rowSelection') && columns.value.unshift(checkColumn);
/********* 选择 相关 start ***********/
let checkAll = ref(false);
let indeterminate = ref(false);
let checkedList = ref([]);
// 全选
function onCheckAllChange(e: any) {
const checked = e.target.checked;
tableData.value.map((item) => {
item._checked = checked;
});
checkedList.value = checked ? getAllCheckedListData(tableSourceData.value, checkedList.value) : [];
indeterminate.value = false;
}
function onCheckBoxChange(record) {
const index = checkedList.value.findIndex((item) => item.id === record.id);
if (index > -1) {
checkedList.value.splice(index, 1);
} else {
checkedList.value.push(record);
}
}
const checkedAlertMessage = computed(() => {
return checkedList.value.length === 0 ? '未选中任何数据' : `已选中 ${checkedList.value.length} 条记录(可跨页)`;
});
// 监听 选择改变
watch(() => checkedList.value, reloadCheckStatus, {
immediate: true,
deep: true,
});
function reloadCheckStatus() {
const { indeterminateStatus, checkAllStatus } = getIndeterminateAndCheckAllStatus(checkedList.value, tableSourceData.value);
indeterminate.value = indeterminateStatus;
checkAll.value = checkAllStatus;
}
// 重置选择
function resetCheckbox() {
onCheckAllChange({ target: { checked: false } });
}
// 给数据添加选项字段
function addCheckedParam(data = []) {
if (!props.hasCheckbox) {
return data;
}
let nData = data;
nData.map((item) => {
const index = checkedList.value.findIndex((check) => item.id === check.id);
item['_checked'] = index !== -1;
});
return nData;
}
/********* 选择 相关 end ***********/
// index序号计算
const tableIndex = computed(() => (index) => (index + unFullRowIndex) / 2 + fullRowIndex + (pageNo.value - 1) * pageSize.value);
let searchData = reactive({});
// 搜索 - 传入搜索数据
function search(sd, reset = true) {
searchData = sd;
reload(reset);
}
// 重载表格
async function reload(reset = false) {
if (reset) pageNo.value = 1;
const res = await props.api({
...searchData,
pageNo: pageNo.value,
pageSize: pageSize.value,
});
// 保存接口数据
sourceData.value = res || [];
tableSourceData.value = addCheckedParam(res.records);
total.value = res.total || 0;
tableData.value = addCheckedParam(filterData(res.records));
reloadCheckStatus();
}
/**
* 修改表格数据
* @param {number} rowIndex 行索引
* @param {string} changeKey 改变的字段
* @param value 要改变的值
*/
function setTableData(rowIndex: number, changeKey: string, value: any) {
if (tableData.value[rowIndex] === undefined) return;
if (tableData.value[rowIndex][changeKey] === undefined) return;
tableData.value[rowIndex][changeKey] = value;
}
function filterData(records) {
return filterSelfTableData(records, props.fullRowKeyList || [], fullRowIndex, unFullRowIndex);
}
onMounted(() => {
hasPagination.value = props.hasPagination || true;
if (props.dataSource) {
// hasPagination.value = false;
sourceData.value = props.dataSource;
tableSourceData.value = addCheckedParam(props.dataSource);
tableData.value = addCheckedParam(filterData(props.dataSource));
} else {
reload(true);
}
});
// 页码改变事件
function pageChange(pNo, pSize) {
pageNo.value = pNo;
// 页数改变时,页码设置1
const reset = pageSize.value !== pSize;
pageSize.value = pSize;
reload(reset);
}
/**
* 获取选中数据 - id
* @return [string | number] 选中数据
*/
function getChecked(): (string | number)[] {
let checkArr: (string | number)[] = reactive([]);
let checkDom: any[] = document.querySelectorAll('input[name="tableCheckbox"]:checked') || [];
checkDom.forEach((check) => {
checkArr.push(check.value);
});
return toRaw(checkArr);
}
/**
* 获取选中数据 - row
* @return [string | number] 选中数据
*/
function getCheckedRows(): (string | number)[] {
// let checkArr: [] = reactive([]);
// let checkDom: any[] = document.querySelectorAll('input[name="tableCheckbox"]:checked') || [];
// let getData = getTableData();
// checkDom.forEach((check) => {
// let checkDataRows = getData.find((item) => item.id == check.value);
// checkArr.push(checkDataRows ? checkDataRows : {});
// });
return toRaw(checkedList.value);
}
// 获取当前表格的数据
function getTableData() {
const tData = tableData.value;
let realData = [];
tData.forEach((item, index) => {
if (index % 2 === fullRowIndex) return;
realData.push({ ...item, ...tData[index + 1] });
});
return realData;
}
/**
* 暴露属性方法
* @param {function} search 搜索方法
* @param {function} getChecked 获取已选中数据方法 - id
* @param {function} getCheckedRows 获取已选中数据方法 - row
* @param {Array} sourceData 获取接口返回的数据
* @param {Array} tableSourceData 获取接口返回的数据中表格的数据
* @param {function} getTableData 获取表格数据
* @param {function} setTableData 设置表格某行的某个单元格数据
*/
defineExpose({
search,
getChecked,
getCheckedRows,
sourceData,
tableSourceData,
getTableData,
setTableData,
});
</script>
<style lang="less" scoped>
.a-alert-cont {
margin-bottom: 8px;
}
.ant-table-striped :deep(.table-striped) td {
background-color: #fafafa;
}
:deep(.ant-table-content) {
border-right: 1px solid #f0f0f0;
}
:deep(.ant-table-thead) > tr > th,
:deep(.ant-table-tbody) > tr > td,
:deep(.ant-table tfoot) > tr > th,
:deep(.ant-table) tfoot > tr > td {
padding: 4px 8px;
}
.my-footer {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.my-checkbox {
cursor: pointer;
width: 16px;
height: 16px;
}
.table-head {
margin-bottom: 8px;
:deep(.ant-btn) {
margin-right: 8px;
}
margin-right: 8px;
}
.head-edit-cont {
display: flex;
align-items: center;
:deep(.vxe-cell--edit-icon) {
border-color: #606266 !important;
margin-right: 3px;
}
}
.full-row-cont {
text-align: left;
}
</style>
selfTableTools.ts
SelfTable组件的依赖工具方法
/**
* 遍历表格数据 为 SelfTable数据类型
* @param data 表格源数据
* @param keys 要放到通行里使用的数据
* @param fullRowIndex 通行索引
* @param unFullRowIndex 非通行索引
*/
export const filterSelfTableData = (data: any[], keys: any[], fullRowIndex: number, unFullRowIndex: number): any[] => {
if (keys.length === 0) return [...data, ...data];
const arr: any[] = [];
data.map((d) => {
const d2 = {};
keys.map((k) => {
d2[k] = d[k];
});
fullRowIndex && arr.push(d);
d2['id'] = d.id;
arr.push(d2);
unFullRowIndex && arr.push(d);
});
return arr;
};
// 过滤表单列
export const filterSelfTableColumns = (columns) => {
return columns.filter((item) => item.ifShow === undefined || item.ifShow() === true);
};
// 通行类型 'prev' 前一行 | 'last' 后一行
export type FullRowType = 'prev' | 'last';
export const useFullRowIndex = (type: FullRowType) => {
const fullRowIndex = type === 'prev' ? 0 : 1;
const unFullRowIndex = type === 'prev' ? 1 : 0;
return { fullRowIndex, unFullRowIndex };
};
// 除第一列外的普通列
export const sharedOnCell = (index, type: FullRowType) => {
// 设置为0时不渲染
return { colSpan: index % 2 === useFullRowIndex(type).fullRowIndex ? 0 : 1 };
};
/**
* 首位列
* @param type 通行位置 prev | last
* @param columnsNum 要合并的行数,也就是总列数,包括操作,不含序号和选择
* @param index 索引
*/
export const firstSharedOnCell = (type: FullRowType, columnsNum: number, index) => ({
colSpan: index % 2 === useFullRowIndex(type).fullRowIndex ? columnsNum : 1,
});
import { TableColumnType } from 'ant-design-vue';
// 表格column类型
export interface SelfTableColumnType extends TableColumnType {
slot?: string;
fullFirst?: boolean;
isEdit?: boolean;
ifShow?: () => boolean;
}
export function checkColumnsHasExist(arr, key): boolean {
return arr.findIndex((item) => item.dataIndex === key) !== -1;
}
/**
* 全选操作时,将所选数据加入到已选择列表中
* @param currentPageData 当前页选中数据
* @param checkedData 已选择的数据
*/
export function getAllCheckedListData(currentPageData: any[], checkedData: any[]) {
const result: any[] = checkedData;
currentPageData.forEach((item) => {
const index = result.findIndex((c) => c.id === item.id);
// 不存在 && 添加
index === -1 && result.push(item);
});
return result;
}
interface CheckAllStatus {
indeterminateStatus: boolean;
checkAllStatus: boolean;
}
/**
* 获取半选状态及全选状态
* @param checkedList 已选列表
* @param currentPageData 当前页数据
*/
export function getIndeterminateAndCheckAllStatus(checkedList, currentPageData): CheckAllStatus {
// 已选数据为空 或者 当前页数据为空
if (!checkedList.length || !currentPageData.length) {
return {
indeterminateStatus: false,
checkAllStatus: false,
};
}
// 筛选当前页面数据未再已选列表的数据
const notCheckedData = currentPageData.filter((item) => {
const index = checkedList.findIndex((c) => c.id === item.id);
return index === -1;
});
return {
indeterminateStatus: notCheckedData.length > 0 && notCheckedData.length < currentPageData.length,
checkAllStatus: notCheckedData.length === 0,
};
}
export const columnColspanFull = (index, fullRowIndex, allColumnNum) => (index % 2 === fullRowIndex ? allColumnNum : 1);
export const columnColspanUnFull = (index, unFullRowIndex) => (index % 2 === unFullRowIndex ? 1 : 0);
export const indexColumnColspan = (index, fullRowIndex, unFullRowIndex, allColumnNum, hasCheckbox) => {
return hasCheckbox ? columnColspanUnFull(index, unFullRowIndex) : columnColspanFull(index, fullRowIndex, allColumnNum);
};
可编辑单元格使用:
页面示例:
使用示例:
主要依赖的就是组件中的setTableData
方法,根据行index,和key去修改表格里面的数据
<SelfTable
ref="selfTableRef"
:has-checkbox="true"
:data-source="dataSource"
:full-row-type="fullRowType"
:columns="selfTableColumns(fullRowType)"
:full-row-key-list="['desc']"
:is-div="true"
:all-table-columns-num="16"
>
<template #fileNumberSlot="scope">
<div class="slot-cont">
<a-input @change="(e) => selfTableRef.setTableData(scope.rowIndex, 'fileNumber', e.target.value)" />
</div>
</template>
<template #inputSlot="scope">
<div class="slot-cont">
<a-input @change="(e) => selfTableRef.setTableData(scope.rowIndex, 'inputValue', e.target.value)" />
</div>
</template>
<template #contractNoSlot="scope">
<div class="slot-cont">
<a-select style="width: 100%" @change="(value) => selfTableRef.setTableData(scope.rowIndex, 'contractNo', value)">
<a-select-option value="jack">Jack</a-select-option>
<a-select-option value="lucy">Lucy</a-select-option>
</a-select>
</div>
</template>
<template #full-row="{ row }"> 其他等级: 资源号: 捆绑号:{{ row.id }}</template>
</SelfTable>
tableColumns方法需要增加isEdit字段,会显示编辑icon
{
title: '件数(件)',
dataIndex: 'fileNumber',
width: 100,
customCell: (_, index) => sharedOnCell(index, fullRowType),
slot: 'fileNumberSlot',
isEdit: true,
},
{
title: '重量(吨)',
dataIndex: 'inputValue',
width: 100,
customCell: (_, index) => sharedOnCell(index, fullRowType),
slot: 'inputSlot',
isEdit: true,
},