前期回顾
目录
封装了一个子组件(table、分页),在不同页面引入
封装的功能有哪些?
分页、表格排序、文字居中、溢出隐藏、操作列、开关、宽、最小宽、type类型(selection/index/expand)、格式化 、不同页面不同操作列、vuex、vue持久化插件、
说思路:data数据请求接口拿到,表头数据一般也是后台接口,如没,前台可自定义自己写
最简单示例
//lable指定表头名 //prop指定每一项数据
<el-table :data="tableData">
<el-table-column
:label="item.label"
:prop="item.prop"
:key="index" v-for="(item,index) in tableHeader"
>
</el-table-column>
</el-table>
//表头
tableHeader:[
{label:'姓名' , prop : 'uname'},
{label:'年龄' , prop : 'age'},
{label:'性别' , prop : 'sex'},
],
//表数据
tableData:[
{uname:"小明",age:'20',sex:'男'},
{uname:"小黑",age:'18',sex:'男'},
]
类似这样:
念及此!那开始正文
Vue3 表格封装
目录结构:
子组件:newTable
<template>
<div class="container">
<el-card shadow="hover">
<el-scrollbar max-height="300px" v-if="isShowSearchRegion" class="wrap">
<slot name="search"></slot>
</el-scrollbar>
<el-table
v-bind="$attrs"
stripe
style="width: 100%"
:data="tableData"
:border="tableBorder"
:height="excludeSearchAreaAfterTableHeight"
>
<template #empty>
<el-empty :image-size="emptyImgSize" description="暂无数据" />
</template>
<el-table-column
type="index"
label="序号"
min-width="60"
:index="orderHandler"
align="center"
/>
<el-table-column
v-for="item in tableHeader"
v-bind="item"
:key="item.prop"
>
<template #default="{ row }" v-if="item.slotKey">
<template v-if="item.slotKey.includes('default')">
<el-link
type="primary"
:underline="false"
@click="handleEdit(row)"
>编辑</el-link
>
<el-popconfirm
title="确定删除吗?"
@confirm="handleDelete(row.id)"
>
<template #reference>
<el-link class="ml15" type="danger" :underline="false"
>删除</el-link
>
</template>
</el-popconfirm>
</template>
<slot
v-for="slot in item.slotKey.split(',')"
:name="slot"
:row="row"
></slot>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-if="paginationFlag"
background
:page-sizes="pageSizesArr"
:current-page="pageNum"
:page-size="pageSize"
:layout="layout"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></el-pagination>
</el-card>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import myEmits from "./newTableConfig/emits";
import myProps from "./newTableConfig/props";
const emits = defineEmits(myEmits);
const props = defineProps(myProps);
// 序号根据数据长度计算
const orderHandler = (index: number) => {
const { pageNum, pageSize } = props;
// 第0条 * 每页条数 + 当前索引+1
return (pageNum - 1) * pageSize + index + 1;
};
// 页数改变
const handleSizeChange = (val: number | string) =>
emits("handleSizeChange", val);
// 当前页改变
const handleCurrentChange = (val: number | string) =>
emits("handleCurrentChange", val);
// 编辑、删除
const handleEdit = (row: object) => emits("handleEdit", row);
const handleDelete = (id: number) => emits("handleDelete", id);
// 搜索区域高度及默认值
const Height = ref();
// 减去搜索区域高度后的table
const excludeSearchAreaAfterTableHeight = ref("calc(100vh - 165px)");
// 获取表格高度-动态计算搜索框高度(onMounted、resize,208是已知的面包屑tebView高度)
const updateHeight = () => {
let wrapEl = document.querySelector(".wrap") as HTMLDivElement | null;
if (!wrapEl) return;
Height.value = wrapEl.getBoundingClientRect().height;
if (props.isShowSearchRegion) {
excludeSearchAreaAfterTableHeight.value = `calc(100vh - ${
165 + Height.value
}px)`;
}
};
onMounted(() => {
// 表格下拉动画
const tableContainer = <HTMLElement>document.querySelector(".container");
setTimeout(() => {
if (tableContainer) tableContainer.style.transform = "translateY(0)";
updateHeight();
}, 300);
});
window.addEventListener("resize", updateHeight);
</script>
<style scoped lang="scss">
.container {
padding: 15px;
transform: translateY(-100%);
transition: transform 0.5s;
// background-color: #870404;
background-color: #f8f8f8;
.el-scrollbar {
// border: 1px solid pink;
min-height: 100px;
width: 100%;
height: fit-content;
}
.el-card {
width: 100%;
height: 100%;
}
.el-pagination {
margin-left: 15%;
height: 35px;
margin-top: 16px;
}
}
// 穿透父组件
:deep(.el-link) {
padding-left: 10px;
}
</style>
抽离的 emits.ts
export default [
// 这里是自定义事件 emits
'handleSizeChange',
'handleCurrentChange',
'handleEdit',
'handleDelete',
];
抽离的 props.ts
export default {
// 表头数据
tableHeader: {
type: Array as () => TableHeader[],
default: function () {
return [];
},
},
// 表格显示的数据
tableData: {
default: function () {
return [];
},
},
// 边框
tableBorder: {
type: Boolean,
default: false,
},
// 总页数
total: {
type: Number,
default: 0,
},
// 分页的页容量数组
pageSizesArr: {
type: Array as () => number[],
default() {
return [10, 20, 30, 50];
},
},
// 分页的布局
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper',
},
// 分页是否显示
paginationFlag: {
type: Boolean,
default: true,
},
// 当前页
pageNum: {
type: Number,
default: 1,
},
// 页容量
pageSize: {
type: Number,
default: 10,
},
// empty的图片大小
emptyImgSize: {
type: Number,
default: 200,
},
// 搜索区域是否显示
isShowSearchRegion: {
type: Boolean,
default: true,
},
// 是否展示连续序号
isSerialNo: {
type: Boolean,
default: true,
},
};
global.d.ts
// new-table
//表头数据类型定义
declare interface TableHeader<T = any> {
label: string;
prop: string;
align?: string;
overHidden?: boolean;
minWidth?: string;
sortable?: boolean;
type?: string;
fixed?: string;
width?: string;
isActionColumn?: boolean;
isCustomizeColumn?: boolean;
slotKey?: string;
}
/*
允许任何字符串作为索引
不然会报错, 使用动态属性名,需要使用索引签名
*/
declare type SearchFormType = {
[key: string]: string;
};
declare type FormOptions = {
type: string;
props: {
label: string;
placeholder: string;
type: string;
clearable: boolean;
};
vm: string;
selectOptions?: [
{
value: string | number;
label: string;
}
];
cascaderOptions?: any;
};
全局css
/* 全局样式 个人不允许修改全局样式【重要!!!】*/
/* 开关样式
------------------------------------------------ */
.el-switch__label--left {
position: relative;
left: 45px;
color: #fff;
z-index: -1111;
}
.el-switch__core {
width: 50px !important;
}
.el-switch__label--right {
position: relative;
right: 46px;
color: #fff;
z-index: -1111;
}
.el-switch__label--right.is-active {
z-index: 1111;
color: #fff !important;
}
.el-switch__label--left.is-active {
z-index: 1111;
color: #fff !important;
}
/* 表格样式
------------------------------------------------ */
.el-table thead tr,
.el-table tbody tr>td {
padding: 0 !important;
height: 75px;
line-height: 75px;
color: #909399;
}
.el-table thead tr>th {
background-color: var(--el-color-info-light-8) !important;
}
newForm
<template>
<el-form ref="searchFormRef" :model="searchForm" size="default">
<!-- 使用了不稳定的 key,可能会导致一些不可预期的行为,比如输入框失去焦点。 -->
<el-row>
<el-col
:xs="24"
:sm="24"
:md="24"
:lg="12"
:xl="6"
v-for="item in formOptions"
:key="item.vm"
>
<el-form-item :label="item.props.label" :prop="item.vm">
<el-input
v-if="item.type === FormOptionsType.INPUT"
v-model.lazy="searchForm[item.vm]"
v-bind="item.props"
class="ml10 w100"
></el-input>
<el-select
v-if="item.type === FormOptionsType.SELECT"
v-model.lazy="searchForm[item.vm]"
v-bind="item.props"
class="ml10 w100"
>
<el-option
v-for="option in item.selectOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
<el-cascader
v-if="item.type === FormOptionsType.CASCADER"
v-model.lazy="searchForm[item.vm]"
:options="item.cascaderOptions"
v-bind="item.props"
class="ml10 w100"
/>
<el-date-picker
v-if="item.type === FormOptionsType.DATE_PICKER"
v-model.lazy="searchForm[item.vm]"
v-bind="item.props"
class="ml10 w100"
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="6" class="xs-mt">
<el-form-item style="margin-left: 10px">
<el-button @click="onReset">
<SvgIcon name="ant-ReloadOutlined"></SvgIcon>
重置
</el-button>
<el-button type="primary" @click="onSearch">
<SvgIcon name="ant-SearchOutlined"></SvgIcon>
查询
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup lang="ts" name="newForm">
import { toRefs, onBeforeUnmount, ref } from 'vue';
import type { PropType } from 'vue';
import { type FormInstance } from 'element-plus';
const searchFormRef = ref<FormInstance>();
enum FormOptionsType {
INPUT = 'input', // 输入框
SELECT = 'select', // 下拉框
CASCADER = 'cascader', // 级联选择器
DATE_PICKER = 'date-picker', // 日期选择器
}
const props = defineProps({
formOptions: {
type: Array as PropType<FormOptions[]>,
required: true,
},
searchForm: {
type: Object as PropType<SearchFormType>,
required: true,
},
});
const { formOptions, searchForm } = toRefs(props);
const emit = defineEmits(['reset', 'search']);
const onReset = () => emit('reset');
const onSearch = () => emit('search');
onBeforeUnmount(() => searchFormRef.value?.resetFields());
defineExpose({ searchFormRef });
</script>
<style scoped lang="scss">
:deep(.el-form-item__label) {
margin-left: 10px;
}
</style>
封装完成,下面在使用的页面调用
页面使用:
<template>
<div class="container-wrapper">
<!-- 动态 page -->
<new-table
:tableHeader="tableHeader"
:tableData="tableData"
:pageNum="pageNum"
:pageSize="pageSize"
:total="pageTotal"
@handleSizeChange="onHandleSizeChange"
@handleCurrentChange="onHandleCurrentChange"
@handleEdit="onHandleEdit"
@handleDelete="onHandleDelete"
>
<template #search>
<new-form
:formOptions="formOptions"
:searchForm="searchForm"
@reset="onReset"
@search="onSearch"
/>
</template>
<template #switch="{ row }">
<el-switch
v-model="row.fileStatus"
active-text="开"
inactive-text="关"
:active-value="1"
:inactive-value="2"
active-color="#13ce66"
inactive-color="#ff4949"
@change="changeSwitchStatus(row.id, row.fileStatus)"
/>
</template>
</new-table>
</div>
</template>
<script setup lang="ts" name="algorithmRegistrationQuery">
import { onMounted, reactive, toRefs } from "vue";
// import { getTestList } from "/@/api/encryptionAlgorithm/templateDefinition";
// import { STATUS_CODE } from "/@/enum/global";
const state = reactive({
//表头数据
// el-table-column有的属性都可以在这传
tableHeader: <TableHeader[]>[
{ label: "姓名", prop: "uname" },
{ label: "年龄", prop: "age" },
{ label: "性别", prop: "sex", slotKey: "switch" },
{ label: "操作", fixed: "right", slotKey: "default" },
],
//表项数据
tableData: [
{ uname: "小帅", age: "18", sex: "男", status: false, id: 1 },
{ uname: "小美", age: "148", sex: "女", status: false, id: 2 },
{ uname: "小明", age: "12", sex: "男", status: true, id: 3 },
{ uname: "小红", age: "12", sex: "女", status: false, id: 4 },
{ uname: "小黑", age: "12", sex: "男", status: true, id: 5 },
{ uname: "小白", age: "12", sex: "女", status: false, id: 6 },
{ uname: "小黑", age: "12", sex: "男", status: true, id: 7 },
{ uname: "小白", age: "12", sex: "女", status: false, id: 8 },
{ uname: "小黑", age: "12", sex: "男", status: true, id: 9 },
{ uname: "小白", age: "12", sex: "女", status: false, id: 10 },
{ uname: "小黑", age: "12", sex: "男", status: true, id: 11 },
],
formOptions: <FormOptions[]>[
{
type: "input",
props: {
label: "合规规则",
placeholder: "请输入合规规则",
type: "text",
clearable: true,
},
vm: "knowledgeName",
},
{
type: "input",
props: {
label: "文件数量",
placeholder: "请输入文件数量",
type: "text",
clearable: true,
},
vm: "documentNumber",
},
// 下拉选择器
{
type: "select",
props: {
label: "所属部门",
placeholder: "请选择",
clearable: true,
},
vm: "department",
selectOptions: [
{
label: "数据安全",
value: 1,
},
{
label: "研发",
value: 2,
},
{
label: "事业",
value: 3,
},
],
},
// 时间范围选择器
{
type: "date-picker",
props: {
label: "时间范围",
type: "datetimerange", // datetimerange范围 datetime日期
clearable: true,
"range-separator": "-",
"start-placeholder": "开始日期",
"end-placeholder": "结束日期",
"value-format": "YYYY-MM-DD HH:mm:ss",
},
vm: "createTime",
},
// 级联选择器
{
type: "cascader",
props: {
label: "所属部门",
placeholder: "请选择",
clearable: true,
},
vm: "cascader",
cascaderOptions: [
{
value: "guide",
label: "Guide",
children: [
{
value: "disciplines",
label: "Disciplines",
children: [
{
value: "consistency",
label: "Consistency",
},
],
},
{
value: "navigation",
label: "Navigation",
children: [
{
value: "side nav",
label: "Side Navigation",
},
{
value: "top nav",
label: "Top Navigation",
},
],
},
],
},
{
value: "component",
label: "Component",
children: [
{
value: "basic",
label: "Basic",
children: [
{
value: "button",
label: "Button",
},
],
},
{
value: "form",
label: "Form",
children: [
{
value: "radio",
label: "Radio",
},
{
value: "checkbox",
label: "Checkbox",
},
],
},
{
value: "data",
label: "Data",
children: [
{
value: "table",
label: "Table",
},
],
},
{
value: "notice",
label: "Notice",
children: [
{
value: "alert",
label: "Alert",
},
],
},
{
value: "navigation",
label: "Navigation",
children: [
{
value: "menu",
label: "Menu",
},
],
},
{
value: "others",
label: "Others",
children: [
{
value: "dialog",
label: "Dialog",
},
],
},
],
},
{
value: "resource",
label: "Resource",
children: [
{
value: "axure",
label: "Axure Components",
},
],
},
],
},
],
//这里允许动态属性所以可为空
searchForm: <SearchFormType>{},
pageNum: 1,
pageSize: 10,
pageTotal: 0,
});
const {
tableHeader,
tableData,
formOptions,
searchForm,
pageNum,
pageSize,
pageTotal,
} = toRefs(state);
// 修改
const onHandleEdit = (row: object) => {
console.log(row);
};
// 删除
const onHandleDelete = (row: object) => {
console.log(row);
};
// switch
const changeSwitchStatus = (id: number, status: boolean) => {
console.log(id, status);
};
//页容量改变
const onHandleSizeChange = (val: number) => {
// console.log('页容量 ==>:', val);
pageSize.value = val;
getTableList(pageNum.value, pageSize.value);
};
//当前分页改变
const onHandleCurrentChange = (val: number) => {
// console.log('当前页 🚀 ==>:', val);
pageNum.value = val;
getTableList(pageNum.value, pageSize.value);
};
// 获取表项数据
const getTableList = (pageNum: number, pageSize: number) => {
// 处理searchForm.value createTime
if (searchForm.value.createTime) {
searchForm.value.startTime = searchForm.value.createTime[0];
searchForm.value.createTimeEnd = searchForm.value.createTime[1];
delete searchForm.value.createTime;
}
// getTestList({
// pageNum,
// pageSize,
// ...searchForm.value,
// }).then((res) => {
// // if (res.code !== STATUS_CODE.SUCCESS) return;
// const { list, total } = res.data;
// tableData.value = list;
// // console.log('🤺🤺 表项 🚀 ==>:', list);
// pageTotal.value = total;
// });
};
// 重置
const onReset = () => {
searchForm.value = {};
getTableList(pageNum.value, pageSize.value);
};
// 查询
const onSearch = () => {
console.log("🤺🤺 查询表单数据 ==>:", searchForm.value);
// 获取表项数据
getTableList(pageNum.value, pageSize.value);
};
onMounted(() => getTableList(pageNum.value, pageSize.value));
</script>
效果:
el-tooltip
子组件:
<template>
<!-- 省略号文字显示不全鼠标放在文字弹框显示所有详情 el-tooltip 子组件 page -->
<el-tooltip :show-after="200">
<template #content>
<div v-html="content" class="text" :style="{ width: myWidth }"></div>
</template>
<slot name="slotView" :num="num"></slot>
</el-tooltip>
</template>
<script setup lang="ts">
let num = 666;
// js
defineProps({
content: {
type: String,
default: '',
},
myWidth: {
type: String,
default: '',
},
});
</script>
<style lang="scss" scoped>
.text {
color: #fff;
word-break: break-all;
word-spacing: 2px;
letter-spacing: 2px;
line-height: 1.5;
font-style: 14px;
}
</style>
父组件:
<template>
<div>
<!-- 父组件 page -->
<mt-tip :content="htmlStr" my-width="700px">
<template v-slot:slotView="slotProps">
<el-button type="warning">按钮——1——————{{ slotProps }}</el-button>
</template>
</mt-tip>
<mt-tip :content="htmlStr" my-width="500px">
<template #slotView="slotProps">
<el-button type="warning">按钮——2——————{{ slotProps }}</el-button>
</template>
</mt-tip>
</div>
</template>
<script setup lang="ts">
import mtTip from './chidren.vue';
let htmlStr = `我是一个字符串你说我是不是我是一个字符串你说我是不是我是一个字符串你说我是不是我是一个字符串你说我是不是我是一个字符串你说我是不是我是一个字符串你说我是不是我是一个字符串你说我是不是我是一个字符串你说我是不是我是一个字符串你说我是不是-·····`;
</script>
<style lang="scss" scoped></style>
el-dialog
子组件:
<template>
<el-dialog
v-bind="$attrs"
v-if="visible"
:title="myTitle"
:width="myWidth"
:before-close="handleClose"
@open="handleOpen"
>
<!-- 封装Dialog子组件 page -->
<div v-loading="myLoading">
<slot name="main"></slot>
<slot name="msg"></slot>
<span class="dialog-footer">
<el-button type="default" @click="cancel">关闭</el-button>
<el-button :type="props.myBtnText[0]" @click="define">{{
props.myBtnText[1]
}}</el-button>
</span>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, watch } from 'vue';
export interface Props {
myTitle: string;
myVisible: boolean;
myWidth?: string;
myLoading?: boolean;
myBtnText?: Array<string>;
}
const props = withDefaults(defineProps<Props>(), {
myWidth: '60%',
myLoading: false,
myBtnText: () => ['primary', '确定'],
});
const emits = defineEmits(['update:modelValue', 'closeDialog', 'closeDefine', 'handleOpen']);
const visible = computed({
get: () => props.myVisible,
set: (value) => {
// console.log('!这里输出 🚀 ==>:', value);
emits('update:modelValue', value);
},
});
//取消按钮事件
const cancel = () => {
emits('closeDialog');
};
// 确定按钮事件
const define = () => {
emits('closeDefine', props.myTitle);
};
// 弹框打开事件
const handleOpen = () => {
emits('handleOpen');
};
// 关闭之前事件
const handleClose = () => {
// console.log('!这里输出 🚀 ==>:', '关闭之前事件');
emits('closeDialog');
};
</script>
<style lang="scss" scoped>
.dialog-footer {
display: flex;
justify-content: center;
margin-top: 20px;
width: 100%;
}
</style>
全局动画弹框css
/* 全局样式 个人不允许修改全局样式【重要!!!】*/
/* 全部dialog进入动画
------------------------------------------------ */
.dialog-fade-enter-active {
display: inline-block;
animation: zoomIn;
animation-duration: 0.5s;
background-color: rgba(0, 0, 0, 0);
}
@keyframes zoomIn {
from {
opacity: 0;
-webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -12.5rem, 0);
transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -12.5rem, 0);
-webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
}
60% {
opacity: 1;
-webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 0.75rem, 0);
transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 0.75rem, 0);
-webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);
}
}
/* 全部dialog离开动画
------------------------------------------------ */
.dialog-fade-leave-active {
display: inline-block;
animation: zoomOut;
animation-duration: 0.5s;
background-color: rgba(0, 0, 0, 0);
}
@keyframes zoomOut {
from {
opacity: 1;
}
60% {
opacity: 0;
-webkit-transform: scale3d(0.3, 0.3, 0.3);
transform: scale3d(0.3, 0.3, 0.3);
}
to {
opacity: 0;
}
}
父组件:
<template>
<div>
<el-button type="primary" size="small" @click="handlerOpen">详情</el-button>
<zw-dialog
@closeDialog="handlerCloseDialog"
@closeDefine="HandleSubmit"
@open="handlerOpen"
v-bind="myProps"
:myLoading="Loading"
v-model="myProps.myVisible"
>
<template #msg> 我是msg </template>
<template #btn="propsSlots">
<el-button type="danger">删除-{{ propsSlots.num }}</el-button>
</template>
</zw-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
// 控制子组件的显示隐藏
const dialogVisible = ref(false);
// 控制子组件的loading
const Loading = ref(false);
// 弹框打开
const handlerOpen = () => {
console.log('弹框打开了');
Loading.value = true;
dialogVisible.value = true;
// 关闭弹框loading 此处应该根据业务逻辑来控制loading的关闭【重要】
setTimeout(() => {
Loading.value = false;
console.log('!这里输出 🚀 ==>:三秒关闭', Loading.value);
}, 3000);
};
// 确定
const HandleSubmit = () => {
dialogVisible.value = false;
console.log('点击了确定');
};
// 取消
const handlerCloseDialog = () => {
dialogVisible.value = false;
console.log('点击了取消');
};
const myProps = ref({
myTitle: '编辑',
myVisible: dialogVisible,
myWidth: '45%',
myLoading: Loading,
myBtnText: ['primary', '保存'],
});
</script>
本文完!