vue3+ts封装表格
时间:2024-12-24 19:43 作者:suxiaojun 分类: 无
更新于2025-12-2;
修改
- 新增搜索栏下拉框配置
- 新增自定义行元素内容或者与行原数据内容并存
- 新增自定义行内元素样式
这里使用了elementplus+vuetify框架封装的
<template>
<div>
<div v-show="showSearch" class="!mb-[8px] !p-6 rounded-b-[16px] bg-[#fff]" :class="className">
<!-- 搜索栏 -->
<el-form :model="form" label-width="auto" :inline="true" label-position="top">
<template v-for="(item, index) in TableHeaders" :key="index">
<el-form-item :label="item.label" v-if="item.search?.component === 'Input'" class="w-[300px]">
<el-input v-model="form[item.field]" :placeholder="item.label" />
</el-form-item>
<el-form-item :label="item.label" v-if="item.search?.component === 'Select'" class="w-[300px]">
<el-select v-model="form[item.field]" :placeholder="item.label" style="min-width: 200px;"
:filterable="item.search?.componentProps?.filterable || false">
<el-option v-for="(optionItem) in item.search?.optionApi?.()" :key="optionItem.label"
:label="optionItem.label" :value="optionItem.value" />
</el-select>
</el-form-item>
<el-form-item :label="item.label" v-if="item.search?.component === 'DatePicker'">
<el-config-provider :locale="zhCn">
<el-date-picker v-model="form[item.field]" :type="item.search?.componentProps?.type || 'daterange'"
range-separator="-" start-placeholder="开始时间" end-placeholder="结束时间"
size="default" :value-format="item.search?.componentProps?.valueFormat || 'YYYY-MM-DD'"
:unlink-panels="item.search?.componentProps?.unlinkPanels || false" />
</el-config-provider>
</el-form-item>
</template>
<el-form-item style="display: block;" class="!flex flex-1 flex-nowrap !items-end mr-0 min-w-[200px]">
<el-button type="primary" color="#5d87ff" class="!text-[#fff]" @click="submit">
查询
</el-button>
<el-button @click="handleForm">重置</el-button>
<slot name="searchButton" :props="form" />
<el-dropdown v-show="SiftHeader" style="margin-left: auto" trigger="click" :hide-on-click="false">
<span class="el-dropdown-link d-flex align-center ga-2">
<img :src="filterSVG" width="20px" alt="">
<span>配置</span>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-checkbox-group v-model="check" @change="changeCheckbox">
<el-dropdown-item v-for="(item, index) in checkList" :key="index">
<el-checkbox :label="item.label" :value="item">
</el-checkbox>
</el-dropdown-item>
</el-checkbox-group>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-form-item>
</el-form>
</div>
<div class="!px-4 rounded-[16px] bg-[#fff]">
<!-- 表格列表 -->
<v-data-table-server :height="height" :headers="tableHeaderList" :items="TableData"
:items-per-page="pagination.itemsPerPage" :page.sync="pagination.page"
:items-per-page-text="'分页页面文本'" show-current-page
:page-text="'页面总数文本' + props.Total" :items-per-page-options="perPageOptions"
:items-length="props.Total" @update:options="switchPagination" :hide-default-footer="HideFooter">
<template v-slot:headers="{ columns }: any">
<tr>
<template v-for="column in columns" :key="column.field">
<th>
<div class="mr-2" :style="{ width: column.width ? column.width : '100px' }">
{{ column.label }}
<v-tooltip v-if="column.labelTipTitle" location="end" :text="column.labelTipTitle">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" icon="mdi-help-circle-outline" size="small"></v-icon>
</template>
</v-tooltip>
</div>
</th>
</template>
</tr>
</template>
<template v-slot:item="{ item }">
<tr class="table-row">
<td v-for="row in tableHeaderList" :key="row.id">
<template v-if="row.field !== 'operate'">
<div v-if="row.customize" :class="row.itemClass">
<!-- vue3动态创建DOM 元素 -->
<component v-if="row?.render" :is="row.render(item, onRenderClick)" @click.stop />
</div>
<div v-else class="flex items-center gap-2" :class="row.itemClass">
<component v-if="row?.render && row.renderFloat === 'left'" :is="row.render(item, onRenderClick)"
@click.stop />
<span v-show="!row.type">
{{ handleFormatter(item, row) }}
</span>
<span v-show="row.type === 'time'">
{{ formatDate(handleFormatter(item, row)) }}
</span>
<component v-if="row?.render && row.renderFloat === 'right'" :is="row.render(item, onRenderClick)"
@click.stop />
</div>
</template>
<template v-else>
<slot name="operate" :item="item" />
</template>
</td>
</tr>
</template>
</v-data-table-server>
</div>
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, reactive, ref, watch, PropType, computed } from 'vue';
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus';
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import en from 'element-plus/es/locale/lang/en';
import { formatDate } from "@/utils/formatDate";
import filterSVG from "@/assets/images/admin-backend/filter.svg";
/**
* @description 可配置相关插槽
* searchButton 搜索框中按钮插槽
* operate 表格中操作列中插槽
*/
interface TableHeader {
field: string; // 字段名
label: string; // 表头名
width?: string; // 列宽
type?: string; // 内容类型
search?: {
hidden?: boolean;
component?: string;
componentProps?: {
type?: any; // 时间选择器:"date" | "year" | "years" | "month" | "months" | "dates" | "week" | "datetime" | "datetimerange" | "daterange" | "monthrange" | "yearrange"
unlinkPanels?: boolean;
valueFormat?: string;
filterable?: boolean;
};
optionApi?: () => Array<{ label: string; value: any }>;
}; // 搜索类型
formatter?: (value: any, row: any) => any; // 格式化函数
render?: (row: any, onRenderClick: any) => any; // 渲染函数
renderFloat?: string; // 渲染位置
itemClass?: string; // 行内元素样式
customize?: boolean; // 是否自定义
rowClick?: (row: any) => void; // 处理行
}
/**
* @param {Array} TableHeaders // 表格表头
* @param {Array} TableData // 表格数据
* @param {Boolean} SiftHeader // 是否使用筛选表头
* @param {Number} Total // 分页总数
* @structure
* field: 对应数据的key
* label: 表头文本
* labelTip: 表头是否显示提示图标,默认flase
* labelTipTitle: 表头提示文本
* type: time 后端返回时间戳时配置类型,为time时转为YYYY-MM-DD hh:mm:ss格式
* search: {
* hidden: 此数据是否使用搜索,默认true
* component: Select下拉框 DatePicker时间筛选 Input输入框
* componentProps: {
* type
* unlinkPanels
* valueFormat
* }
* }
* formatter: function
*/
const props = defineProps({
className: {
type: String,
default: undefined
},
TableHeaders: {
type: Array as () => TableHeader[],
default: () => [] // 添加默认值
},
TableData: Array as () => any,
SiftHeader: {
type: Boolean,
default: false
},
SiftHeaderTitle: {
type: String,
default: ''
},
Total: {
type: Number,
default: 0
},
HideFooter: Boolean,
onRenderClick: Function as PropType<(row: any, value: any) => void>,
height: {
type: String,
default: undefined
},
});
// 默认分页数和数据量
const pagination = reactive({
page: 1,
itemsPerPage: 10, // 每页显示的行数
});
const perPageOptions = reactive([
{ value: 10, title: '10' },
{ value: 25, title: '25' },
{ value: 50, title: '50' },
{ value: 100, title: '100' }
]);
/**
* @description 表格表头
* 为了自定义表头数据,不使用props中的数据而是重新定义变量接收
*/
const tableHeaderList: any = ref([]);
/* 搜索表单 */
const form: any = reactive({});
/**
* 显示搜索表单控件
* 如果表头中存在需要搜索条件则为true
*/
const showSearch = ref(false);
// 筛选默认勾选
const check: any = ref(null);
// 筛选列表
const checkList: any = computed(() => {
return props.TableHeaders.map((item: any) => ({
field: item.field,
label: item.label,
}));
});
/**
* @description 处理搜索表单和筛选
*/
const handleForm = () => {
// 筛选是否设置了hidden
props.TableHeaders.forEach((item: any) => {
if (!item.search.hidden) {
form[item.field] = ''
showSearch.value = true
}
})
};
/* 多选框发生变化时触发 */
const changeCheckbox: any = (value: string[]) => {
// 原数组对比组件中返回的数组,然后进行重组
const list = props.TableHeaders.map((item: any) => {
let data = null;
value.forEach((row: any) => {
if (row.field === item.field) {
data = item
return
};
});
return data
});
// 剔除掉要隐藏的对象
tableHeaderList.value = list.filter((row: any) => {
return row != null
});
if (props.SiftHeader) {
localStorage.setItem(props.SiftHeaderTitle, JSON.stringify(check.value))
}
};
watch(
[() => props.TableHeaders,
([newHeaders, newLocale]) => {
// 初始化 checkList
checkList.value = newHeaders.map((item: any) => ({
field: item.field,
label: item.label,
}))
// 获取本地缓存
const localSiftHeader = JSON.parse(localStorage.getItem(props.SiftHeaderTitle) || '[]')
if (props.SiftHeader && localSiftHeader.length > 1) {
// 使用map筛选出本地缓存存在的数据
const mapCheck = newHeaders.map((item: any) => {
const matchingRow = localSiftHeader.find((row: any) => row.field === item.field);
if (matchingRow) {
return {
field: item.field,
label: item.label,
};
}
return null;
});
// 去除掉筛选出后的数据里返回的null
check.value = mapCheck.filter((row: any) => row !== null);
} else {
check.value = newHeaders.map((item: any) => ({
field: item.field,
label: item.label,
}))
}
// 重新调用 changeCheckbox 以应用新的表头
changeCheckbox(check.value)
},
{ immediate: true, deep: true }
);
const emit = defineEmits(['onSubmit', 'pagination']);
/**
* @description 提交搜索表单
* 父组件使用@onSubmit 绑定事件
*/
const submit = () => {
emit('onSubmit', form);
};
/**
* @description 切换分页
* 父组件使用@pagination 绑定事件
*/
const switchPagination = (options: any) => {
pagination.page = options.page;
pagination.itemsPerPage = options.itemsPerPage;
emit('pagination', options);
};
/**
* @description 处理formatter
* @param item 列表数据
* @param row 表头定义对象
*/
const handleFormatter = (item: any, row: any) => {
if ('formatter' in row) {
return row.formatter(item[row.field], item)
} else {
return item[row.field]
}
};
const isMobile = ref(false);
// 判断是否为移动端
const checkDevice = () => {
isMobile.value = window.innerWidth <= 768 || /android/i.test(navigator.userAgent) || /iphone|ipod/i.test(navigator.userAgent);
};
onMounted(() => {
handleForm();
// 监听窗口大小变化
window.addEventListener('resize', checkDevice)
});
// 组件销毁时移除事件监听器
onBeforeUnmount(() => {
window.removeEventListener('resize', checkDevice);
});
</script>
<style lang="scss" scoped>
.table-row {
transition: background-color 0.3s ease-out;
}
.table-row:hover {
background-color: #497ff917;
}
::v-deep(.el-form-item__label-wrap) {
margin-left: 0 !important;
}
::v-deep(.el-input__wrapper.is-focus),
::v-deep(.el-range-editor.is-active),
::v-deep(.el-select__wrapper.is-focused) {
box-shadow: 0 0 0 1px #5d87ff !important;
}
::v-deep(.el-button:hover) {
background-color: #5d88ff4b;
border-color: #5d88ff56;
color: #5d87ff;
outline: none;
}
::v-deep(.el-checkbox__inner:hover) {
border-color: #5d87ff;
}
::v-deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: #5d87ff;
border-color: #5d87ff;
}
::v-deep(.el-checkbox__input.is-checked+.el-checkbox__label) {
color: #5d87ff;
}
</style>
下面是使用这个封装组件的父组件
HTML文件
<OperateTable :TableHeaders="ActivationTableHeaderList" :TableData="ActivationTableDataList"
:Total="total" @onSubmit="(childParams: any) => subSearch('Activation')(childParams)"
@pagination="(childParams: any) => switchPagination"
:on-row-click="handleRowClick">
<template v-slot:operate="{ item }">
<v-menu open-on-hover :v-model="selectOperate">
<template v-slot:activator="{ props }">
<v-icon icon="mdi-dots-horizontal" start v-bind="props" />
</template>
<v-list class="theme-list">
<v-list-item v-for="(operation, index) in operationList" :key="index" color="primary"
:ripple="true" @click="handleAction(operation, item)">
<v-list-item-title class="text-subtitle-1 font-weight-regular">
{{ operation.label }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</template>
</OperateTable>
JS部分
import { ref, reactive, onMounted, computed, provide, h } from 'vue';
/* 为了使用动态使用icon引入组件 */
import { VIcon } from 'vuetify/components';
/* 列表数据 */
const TableDataList: any = ref([])
/* 搜索查询 */
const getList = async (value: string) => {
const data: any = {
page: tablePage.value,
size: itemsPerPage.value,
...searchFrom.value
}
const res: any = await getListApi(data)
if (res.code !== 0) return
TableDataList.value = res.data.list
total.value = res.data.total
}
/* 动态DOM元素的事件 */
const handleRowClick = async (row: any, value: string) => {
const params = {
key: row[value]
}
const res: any = await decryptApi(params)
if (res.code !== 0) return
};
/* 定义表头 */
const TableHeaderList = computed(() => {
return ([
{
field: 'field',
label: '不需要搜索',
width: '130px',
search: {
hidden: true
}
},
{
field: 'field',
label: '输入框搜索',
width: '60px',
search: {
component: 'Input',
}
},
{
field: 'time',
label: '时间格式搜索',
type: 'time',
search: {
component: 'DatePicker',
componentProps: {
type: 'daterange',
unlinkPanels: true,
valueFormat: 'YYYY-MM-DD'
}
}
},
{
field: 'field',
label: '下拉菜单搜索和根据数据回显',
width: '130px',
search: {
component: 'Select',
optionApi: () => {
const res = [
{
value: 0,
label: '下拉选项1'
},
{
value: 1,
label: '下拉选项2'
}
]
return res
}
},
formatter: (cellValue: string | number) => {
let typeText: string | number = ''
switch (cellValue) {
case 0:
typeText = '回显数据1'
break
case 1:
typeText = '回显数据1'
break
default:
typeText = cellValue
break
}
return typeText
}
},
{
field: 'field',
label: '自定义行元素内容',
width: '130px',
search: {
hidden: true
},
customize: true,
render: (row: any) => {
return h(
'div',
{},
`${row.pay_amount} ${row.pay_coin_name}`
)
}
},
{
field: 'field',
label: '在原有行元素内容增加一个自定义内容并配置属性',
width: '160px',
search: {
hidden: true
},
renderFloat:left,
// Vue3特性--动态插入DOM元素
render: (row: any) => h(VIcon, { icon: 'mdi-eye-outline', style: 'color: #939a9d; cursor: pointer;', onClick: () => handleRowClick(row, '') })
},
{
field: 'operate',
label: t("operate"),
width: '50px',
search: {
hidden: true
}
}
])
});