Home
avatar

.Wang

表格合并组件配置

最近的需求有一个这样的功能,单元格合并,听起来很简单是不是,那么继续往下看。 首先我先大致介绍一下项目技术栈:v3 + ts + element-plus + vxe-table 相关。

项目中涉及到的表格数据都是使用的vxe-table框架,于是发现vxe-table并不满足我现在的需求,于是就有了自定义合并单元格的需求。

首先看下原型:

iShot_2025-10-21_13.45.30.png

我的想法

每一行都可以作为一个组件,而标题,副标题,展开收起的状态,表格的表格都是可以进行配置的。

再加上使用的是vxe-table框架,那么就可以直接在vxe-table中进行配置, 接下来改造一下vxe-table的配置。

// 执行价格
interface CUSTOM_COLUMN extends VxeTableDefines.ColumnOptions {
	// 合并表头单元格
	headerMergeCells?: number;
}
// 继承原有的vxe-table的配置
export interface CUSTOM_EXECUTE_PRICE extends VxeGridProps {
	columns: CUSTOM_COLUMN[];
}
const EXECUTE_PRICE_COLUMN: CUSTOM_EXECUTE_PRICE = {
	// 这俩列是固定不变的,
	columns: [
		{
			title: "全国价(元)",
			width: 200,
			slots: { default: "nationalPrice" },
		},
		{
			title: "专属价(元)",
			width: 200,
			slots: { default: "exclusivePrice" },
		},
	],
	data: [],
};

// 基础价格配置
export const baseTableConfig = cloneDeep({
	...EXECUTE_PRICE_COLUMN,
	columns: [
		{
			title: "属性名称",
			slots: { default: "name" },
			headerMergeCells: 1,
		},
		...EXECUTE_PRICE_COLUMN.columns,
	],
});
// 颜色价格配置
export const colorTableConfig = cloneDeep({
	...EXECUTE_PRICE_COLUMN,
	columns: [
		{
			title: "属性名称",
			slots: { default: "name" },
			headerMergeCells: 1,
		},
		...EXECUTE_PRICE_COLUMN.columns,
	],
});

// 规格价格配置
export const specTableConfig = cloneDeep({
	...EXECUTE_PRICE_COLUMN,
	columns: [
		{
			title: "属性名称",
			slots: { default: "name" },
			headerMergeCells: 2,
		},
		...EXECUTE_PRICE_COLUMN.columns,
	],
});

// 行业价格配置
export const industryTableConfig = cloneDeep({
	...EXECUTE_PRICE_COLUMN,
	columns: [
		{
			title: "属性名称",
			slots: { default: "name" },
			headerMergeCells: 2,
		},
		...EXECUTE_PRICE_COLUMN.columns,
	],
});

这样的话,我们的这个表格配置就基本上是这样的。

接下来,需要去封装一个组件,来实现这个功能。

<script setup lang="ts">
import { CUSTOM_EXECUTE_PRICE } from "../config/table";

defineOptions({
	name: "ExecutePriceDialogItem",
});
const props = defineProps<{
	title: string;
	tipText?: string;
	gridOption: CUSTOM_EXECUTE_PRICE;
}>();
const expand = ref(true);
const tableConfig = ref(props.gridOption);
const colspan = computed(() => {
	return tableConfig.value.columns.reduce((prev, cur) => {
		return prev + (cur.headerMergeCells || 1);
	}, 0);
});
watch(
	() => props.gridOption,
	newVal => {
		tableConfig.value = newVal;
	},
	{ deep: true, immediate: true }
);
</script>

<template>
	<div class="price-item">
		<div class="header">
			<h3 class="title">{{ title }}</h3>
			<div class="expand-icon" @click="expand = !expand">
				{{ expand ? "- 收起" : "+ 展开" }}
			</div>
			<div class="tip-text">{{ tipText }}</div>
		</div>
		<div v-if="expand" class="price__table">
			<!-- 因为vxe-table 的最新版本,不支持表头合并,所以这里使用原生table -->
			<table>
				<thead>
					<tr>
						<th
							v-for="item in tableConfig.columns"
							:key="item.field"
							:colspan="item.headerMergeCells || 1"
							:width="item.width"
						>
							{{ item.title }}
						</th>
					</tr>
				</thead>
				<tbody>
					<template v-if="tableConfig.data!.length > 0">
						<!-- 这里将数据作为插槽的形式 -->
						<tr v-for="(item, index) in tableConfig.data" :key="index">
							<slot :row="item" :row-index="index" />
						</tr>
					</template>
					<template v-else>
						<tr>
							<td :colspan="colspan">暂无数据</td>
						</tr>
					</template>
				</tbody>
			</table>
			<!-- 使用vxe-table: 表头不能自定义与合并,与原型设计不符 -->
			<!-- <vxe-grid v-bind="tableConfig" ref="vxeGridRef">
        <template
          v-for="col in tableConfig.columns?.filter((c) => c.slots?.default)"
          :key="col.field"
          #[col.slots!.default]="{ row }"
        >
          <slot :name="col.slots!.default" :row="row" />
        </template>
      </vxe-grid> -->
		</div>
	</div>
</template>

<style lang="scss" scoped>
.price-item {
	.header {
		display: flex;
		align-items: center;
		.title {
			margin-right: 12px;
		}
		.expand-icon {
			cursor: pointer;
			color: var(--el-color-primary);
		}
		.tip-text {
			flex: 1;
			text-align: right;
			color: #4dbc15;
		}
	}

	:deep() {
		.price__table {
			table {
				width: 100%;
				border-collapse: collapse;
				th,
				td {
					border: 1px solid #eee;
					padding: 8px;
					text-align: center;
				}
				th {
					background-color: #f9f9f9;
					color: #111;
					font-weight: bold;
				}
			}
		}
	}
}
</style>

子组件完成之后,接下来完成父组件的引入

<template>
	<!-- 不需要合并的配置 -->
	<executePriceDialogItem
		:grid-option="state.baseTableConfig"
		title="基础价格配置"
	>
		<template #default="{ row }">
			<td></td>
			<td></td>
			<td></td>
		</template>
	</executePriceDialogItem>

	<!-- 需要合并的配置 -->
	<executePriceDialogItem
		:grid-option="state.specTableConfig"
		title="规格价格配置"
	>
		<template #default="{ row }">
			<!-- _cell:表示需要合并的单元格数量 -->
			<td v-if="row._cell" :rowspan="row._cell" style="width: 200px"></td>
			<td></td>
			<td></td>
			<td></td>
		</template>
	</executePriceDialogItem>
</template>

那么这样一个组件就可以同步使用其他的场景了,当然数据格式是需要自己去整理的。

工作总结