Skip to content

Sticky 模式表格

优点

  1. 冻结列不用单独渲染,只渲染一个表格,性能至高提升200%;
  2. 解决冻结列二次渲染带来的数据状态问题;
  3. 完全解决冻结列行高不一致的问题;

缺点

  1. 合并列表格、树形表格可能有问题;
  2. 暂不支持横向虚拟渲染;
vue
<template>
	<fk-space direction="vertical" style="width: 100%">
		<VxeGrid ref="grid" style="width: 100%" class="config-table" v-bind="gridOptions" v-on="gridEvents">
			<template #goods_multi_field="{ row }">
				<div class="tw-w-[100%] tw-flex tw-items-start tw-gap-x-[10px]">
					<div class="tw-rounded-[4px]">
						<ShopUpload v-if="row.product_default_pic?.path" v-model="row.product_default_pic.path" :width="56" :height="56" :disabled="true" />
						<img v-else :src="`${AssetsServer}/image/main/goods-placeholder.png`" style="width: 56px; height: 56px; border-radius: 4px" />
					</div>
					<div class="tw-flex tw-flex-col tw-gap-y-[3px] tw-flex-1 tw-overflow-hidden">
						<div class="tw-flex tw-items-center">
							<Tooltip :content="row.shape_code">
								<div class="shape-code">
									{{ row.shape_code }}
								</div>
							</Tooltip>
							<i class="erpfont icon-eye tw-cursor-pointer tw-text-[#86909C] tw-ml-[5px] hover:tw-text-[#165DFF]" />
							<i class="erpfont icon-copy tw-cursor-pointer tw-text-[#86909C] tw-ml-[5px] hover:tw-text-[#165DFF]" />
						</div>
						<div class="tw-whitespace-nowrap tw-flex tw-items-center tw-gap-[5px]">
							<span class="tw-text-[12px] tw-text-[#4E5969]"> 款号简称:{{ row.simple_name || '--' }} </span>
							<Tooltip :content="row.product_edit_detail">
								<img
									v-if="row.product_edit"
									:src="`${AssetsServer}/image/main/icon_edit.svg`"
									style="width: 14px; height: 14px; border-radius: 50%"
								/>
							</Tooltip>
						</div>
						<div class="tw-whitespace-nowrap tw-text-[12px] tw-text-[#4E5969]">创建时间:{{ dayjs(row.create_time).format('YYYY-MM-DD') }}</div>
					</div>
				</div>
			</template>
			<template #product_default_pic="{ row }">
				<ShopUpload v-if="row.product_default_pic?.path" v-model="row.product_default_pic.path" :width="56" :height="56" :disabled="true" />
			</template>
			<template #product_name="{ row }">
				<div>
					<Tooltip :content="row.product_name">
						<div class="line-clamp">
							{{ row.product_name }}
						</div>
					</Tooltip>
				</div>
			</template>
			<template #status="{ row }">
				<div class="tw-flex tw-items-center tw-gap-[8px]">
					<Switch v-model="row.status" :checked-value="1" :unchecked-value="2" />
					<span class="tw-text-[#4E5969]">
						{{ row.status === 1 ? '已启用' : '已禁用' }}
					</span>
				</div>
			</template>
			<template #put_status="{ row }">
				<div class="tw-flex tw-items-center tw-gap-[8px]">
					<DropdownButton>
						{{ row.put_status === 1 ? '已上架' : '仓库中' }}
						<template #icon>
							<IconDown />
						</template>
						<template #content>
							<Doption v-if="row.put_status === 2" :value="1">
								<span class="tw-px-[10px]">上架</span>
							</Doption>
							<Doption v-if="row.put_status === 1" :value="2">
								<span class="tw-px-[10px]">下架</span>
							</Doption>
						</template>
					</DropdownButton>
				</div>
			</template>
			<template #last_price="{ row }">
				{{ row?.last_price?.price }}
			</template>
			<template #mark="{ row }">
				<span v-if="row.mark.length < 3" class="tw-flex tw-items-center tw-flex-wrap tw-gap-[5px]">
					<Tag v-for="item in row.mark" :key="item.mark_id" bordered class="mark-tag mark">
						{{ item.value }}
					</Tag>
				</span>
				<span v-else class="tw-flex tw-items-center tw-flex-wrap tw-gap-[5px]">
					<Tag v-for="item in row.mark.slice(0, 2)" :key="item.mark_id" bordered class="mark-tag mark">
						{{ item.value }}
					</Tag>
					<Popover>
						<Tag class="mark-tag" bordered>+{{ row.mark.length - 2 }}</Tag>
						<template #content>
							<Tag v-for="item in row.mark.slice(2, row.mark.length)" :key="item.mark_id" bordered class="mark-tag">
								{{ item.value }}
							</Tag>
						</template>
					</Popover>
				</span>
			</template>
			<template #source_from="{ row }">
				{{ row.source_from }}
			</template>
			<template #category_name="{ row }">
				<div class="line-clamp">
					{{ row?.category_name }}
				</div>
			</template>
			<template #shape_code="{ row }">
				<div class="line-clamp">
					{{ row.shape_code }}
				</div>
			</template>
			<template #sex_code="{ row }">
				{{ row.sex_code }}
			</template>
			<!-- 供应商款号 -->
			<template #product_supplier="{ row }">
				<div v-if="row.product_supplier.length < 2">
					<span v-for="item in row.product_supplier" :key="item.id">
						{{ item.supplier_shape_code }}
					</span>
				</div>
				<div v-else class="tw-flex tw-items-center tw-flex-wrap tw-gap-[10px]">
					<span v-for="item in row.product_supplier.slice(0, 1)" :key="item.id">
						{{ item.supplier_shape_code }}
					</span>
					<Popover>
						<Tag class="mark-tag" bordered>+{{ row.product_supplier.length - 1 }}</Tag>
						<template #content>
							{{
								row.product_supplier
									.slice(1, row.product_supplier.length)
									.map(e => e.supplier_shape_code)
									.join(',')
							}}
						</template>
					</Popover>
				</div>
			</template>
			<!-- 供应商 -->
			<template #product_supplier_name="{ row }">
				<div class="line-clamp">
					{{ (row.product_supplier || []).map(e => e.supplier?.name)?.join(',') }}
				</div>
			</template>
			<template #unit="{ row }">
				{{ row.unit }}
			</template>
			<template #remark="{ row }">
				<div class="line-clamp">
					{{ row.remark }}
				</div>
			</template>
			<template #operator>
				<Button size="small" type="text">详情</Button>
				<Button size="small" type="text"> 编辑 </Button>
			</template>
		</VxeGrid>
	</fk-space>
</template>

<script setup lang="tsx">
import { onMounted, reactive, ref, useTemplateRef } from 'vue';
import dayjs from 'dayjs';
import { AssetsServer, ShopUpload, VxeGrid, http, mergeGridProps } from '@erp/biz';
import { Button, Doption, DropdownButton, IconDown, Popover, Switch, Tag, Tooltip } from '@erp/common';
import type { VxeGridInstance, VxeGridListeners } from '@erp/biz';

/**
 * 引用template模板
 */
const gridRef = useTemplateRef<VxeGridInstance>('grid');

const sortObj = ref({});

/**
 * 表格 grid 配置
 */
const gridOptions = reactive(
	mergeGridProps({
		id: 'goodsList',
		optimize: 'sticky',
		height: 660,
		columnConfig: {
			width: 160,
		},
		toolbarConfig: {
			buttons: [
				{
					label: '重新请求数据',
					code: 'getData',
					type: 'primary',
				},
			],
		},
		checkboxConfig: {
			isShiftKey: true,
			trigger: 'cell',
		},
		rowConfig: {
			isCurrent: false,
		},
		showOverflow: false,
		// scrollY: null,
		scrollY: {
			enabled: true,
			gt: 0,
			mode: 'wheel',
			oSize: 6,
			// scrollToTopOnChange: true,
		},
		scrollX: null,
		mouseConfig: {
			area: true,
			selected: true,
		},
		areaConfig: {
			autoClear: true,
			// areaStatusTeleportTo: '.vxe-grid--pager-status',
		},
		columns: [
			{
				field: 'checkbox',
				type: 'checkbox',
				width: 55,
				align: 'center',
				headerAlign: 'center',
				fixed: 'left',
				showOverflow: false,
				visible: 'local',
			},
			{
				type: 'seq',
				width: 55,
				align: 'center',
				headerAlign: 'center',
				fixed: 'left',
				visible: 'local',
			},
			...[
				'goods_multi_field',
				'product_default_pic',
				'product_name',
				'last_price',
				'category_name',
				'status',
				'put_status',
				'sex_code',
				'unit',
				'mark',
				'product_supplier',
				'source_from',
				'product_supplier_name',
				'remark',
			].map(v => {
				return {
					field: v,
					slots: { default: v },
				};
			}),
			{
				field: '#operator',
				title: '操作',
				width: 140,
				slots: {
					default: 'operator',
				},
				fixed: 'right',
				visible: 'local',
			},
		],
		data: [],
	}),
);

const getGoodsList = params => {
	return http.post('/product/index', params);
};

const getList = async () => {
	gridOptions.loading = true;
	const { list, total } = await getGoodsList({
		page: gridOptions.pagerConfig!.current,
		page_size: gridOptions.pagerConfig!.pageSize,
		status: 1,
		...sortObj.value,
	});
	gridOptions.data = list;

	gridOptions.pagerConfig!.total = total;
	gridOptions.loading = false;

	console.log('gridOptions.data >>', gridRef.value.getColumns());
};

const gridEvents: VxeGridListeners = {
	toolbarButtonClick: ({ code, $grid }) => {
		if (code === 'getData') {
			getList();
		}
	},
	cellAreaSelectionStart: params => {
		console.log('cellAreaSelectionStart >>', params);
	},
	cellAreaSelectionEnd: params => {
		console.log('cellAreaSelectionEnd >>', params);
	},
	cellAreaSelectionChange: params => {
		console.log('cellAreaSelectionChange >>', params);
	},
	clearCellAreaSelection: params => {
		console.log('clearCellAreaSelection >>', params);
	},
	pageChange({ pageSize, current }) {
		gridOptions.pagerConfig!.current = current;
		gridOptions.pagerConfig!.pageSize = pageSize;
		getList();
	},
	sortChange({ field, order }) {
		sortObj.value = order
			? {
					sortField: field,
					sortOrder: order,
			  }
			: {};
		getList();
	},
};

onMounted(getList);
</script>

<style lang="less" scoped>
:deep(.fk-input-tag) {
	background: transparent;
	.fk-tag {
		border-color: #e5e6eb !important;
	}
}
.shape-code {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	color: #1d2129;
	font-weight: 600;
	cursor: pointer;
	border-bottom: 1px solid transparent;
}
.shape-code:hover {
	border-bottom: 1px solid #165dff;
	color: #165dff;
}
.line-clamp {
	overflow: hidden;
	text-overflow: ellipsis;
	display: -webkit-box;
	-webkit-line-clamp: 3;
	-webkit-box-orient: vertical;
}
.mark-tag {
	display: flex;
	justify-content: center;
	border: 1px solid #165dff;
	color: #165dff;
	background: #e8f3ff;
	cursor: pointer;
}
.mark {
	min-width: 70px;
	max-width: 100px;
}
:deep(.table-list .vxe-toolbar) {
	padding: 0;
}
</style>

基于 MIT 许可发布