第四节:剖析表格组件的封装思路(表格配置、分页条、页面传值、高度自适应、单选、多选等)

一.  前言

1.  封装的表格组件功能

  (1).  通过配置文件的形式渲染表格列,并且表格列的格式可以自定义修改

  (2).  表格展开功能

  (3).  表格列排序

  (4).  表格单选、多选

  (5). 表格表头支持两行模式

  (6). 表格列合并、行合并

  (7). 表格高度自适应

  (8). 表格动态显示分页条

 

2. 涉及的技术点

 (1). 页面传值

 (2). 命名插槽

 (3). v-model 等价   :xxx  + @update:xxx 

 (4). 其它等等

 

二.  表格组件核心封装  【未完】

1. 组成

   表格包含三部分:按钮区域、表格内容区域、分页条区域。
 

2. 核心部分

(1). 顶部按钮区域

   两个命名插槽,分别为btnLeft、btnRight,一左一右,用来显示按钮区域。

<!-- 1 按钮区域 -->
<div class="myBtnArea">
    <div><slot name="btnLeft"></slot></div>
    <div><slot name="btnRight"></slot></div>
</div>

调用:

	<ypfTable
			ref="ypfTableRef"
		>
			<!-- 2.1 按钮区域 -->
			<template #btnLeft>
				<el-button size="small" type="success" @click="onOpenAddDialog" v-auth="authList.add"> 新增 </el-button>
				<el-button size="small" type="danger" @click="deleteObjs(null)" v-auth="authList.delMany"> 删除 </el-button>
			</template>
			<template #btnRight>
				<el-button size="small" type="primary" round v-auth="authList.excel" @click="exportExcel"> 导出 </el-button>
				<el-button size="small" type="success" round v-auth="authList.arrange" @click="openSettingDialog"> 设置 </el-button>
			</template>

		</ypfTable>

(2). 中间表格区域

    date绑定数据源;v-bing绑定表格属性;selection-change监听多选;current-change监听单选;sort-change监听列排序;span-method用来处理表格行、列合并事件。

    另外,每一个表格列除了使用el-table-column默认的插槽外,在此之上,又借助传递的配置中的slotName字段,声明自己的命名插槽,便于表格自定义格式。

<!-- 2. 表格区域    -->
		<div><slot name="tableTitle"></slot></div>
		<el-table
			size="default"
			ref="tableRef"
			:[myHeightProp]="`calc(100vh - ${myDynamicHeight})`"
			:data="tableRows"
			v-bind="tableProp"
			@selection-change="tableSelChange"
			@current-change="tableSimpleSelChange"
			@sort-change="sortChange"
			:span-method="mergeFn"
		>
			<el-table-column v-if="showSelectColumn" type="selection" width="26" />
			<template v-for="propItem in propList" :key="propItem.prop">
				<el-table-column  v-bind="propItem">
					<!-- 2. 普通表格,不含多级表头 -->
					<template #default="scope">
							<!-- :name,属于具名插槽,有默认值,说明外界可以自己传进来,也可以使用默认值-->
							<!-- :row1="scope.row" 是插槽prop,对外传值 -->
							<!-- (1).如果是匿名插槽,外界v-slot:default="myInfo"接收,然后myInfo.row1.xxx调用,然后可以省略为v-slot="myInfo" -->
							<!-- (2).如果是具名插槽,外界v-slot:name="myInfo"接收, 然后myInfo.row1.xxx调用, 省略为 #name="myInfo",这里的name是实际的propItem.slotName此处是具名插槽! -->
						<slot :name="propItem.slotName" :row1="scope.row">
							<!-- 下面是默认值,只有当没有提供插入内容的时候才会显示 -->
							{{ scope.row[propItem.prop] }}
						</slot>
					</template>
				</el-table-column>
			</template>
		</el-table>

 调用: 

	<ypfTable
			ref="ypfTableRef"
			v-bind="contentTableConfig"
			:tableRows="tableData.tableRows"
			:propList="bindPropList"
			:tableTotal="tableData.total"
			:page="tableData.param"
			@update:page="UpdatePage"
			@update:tableSelChange="tableSelChange"
			@update:sortChange="sortChange"
		>
			<!-- 2.2 表格区域 -->
			<template #userSex="myInfo">
				<el-tag type="success" v-if="myInfo.row1.userSex == 0">男</el-tag>
				<el-tag type="info" v-else>女</el-tag>
			</template>
			<template #addTime="myInfo">
				{{ formatDate(new Date(myInfo.row1.addTime), 'YYYY-mm-dd') }}
			</template>
			<template #handler="myInfo">
				<el-button size="small" type="text" @click="onOpenEditDialog(myInfo.row1)" v-auth="authList.edit">修改</el-button>
				<el-button size="small" type="text" @click="deleteObjs(myInfo.row1.id)" v-auth="authList.delOne">删除</el-button>
			</template>

			<!-- 2.3 分页区域 -->
		</ypfTable>

(3). 底部分页条区域

  具体的传值和监听往后看。

<!-- 2.3 分页区域 -->
		<el-pagination
			v-if="showPagination"
			style="display: flex; justify-content: center"
			layout="total, sizes, prev, pager, next, jumper"
			class="mt15"
			background
			:page-sizes="[5, 10, 20, 30]"
			:total="tableTotal"
			v-model:current-page="myPage.pageNum"
			v-model:page-size="myPage.pageSize"
		>
		</el-pagination>

 

3. 如何实现表格列的展开?

   此处要包含表格展开的写法:核心点,需要在第一个 el-table-colum 上加 type="expend"  (结合新船系统,表格嵌套表格的样例)

即配置文件的第一列:

const contentTableConfig = {
	tableTitle: 'List of Drawings',
	//表格显示列和排列顺序(默认的) ,该对象即使绑定了也不使用, 最终绑定生效的是处理后的对象bindPropList
	originPropList: [
		{ type: 'expand', fixed: 'left', prop: 'spread', label: '', align: 'center', slotName: 'spread' },
		{ prop: 'x1', label: '*SI No.', align: 'center' },
		{ prop: 'x12', label: 'Verified', align: 'center' },
		{ prop: 'x13', label: '*Planned Submission Date', align: 'center', width: '110px' },
		{ prop: 'x14', label: 'Iteration Count', align: 'center' },
	],
	// 开启多选列
	showSelectColumn: false,
	// 是否显示底部分页
	showPagination: false,
};

页面调用的时候,即表格组件内部嵌套

#spread="myInfo"  表示表格第一列的命名插槽

<ypfTable v-bind="contentTableConfig" :tableRows="myTableData" :propList="contentTableConfig.originPropList" :tableTotal="1">
			<!-- 2.2 表格区域 -->
			<template #spread="myInfo">
				<div class="mySpread">
					<div>
						<div>
							<el-button size="small" type="primary" @click="onOpenAddDialog(myInfo.row1.x1, 1)">Add Comment</el-button>
							<el-button size="small" type="primary" @click="onOpenEditDialog(selRowObj1, myInfo.row1.x1, 1)">Edit comment</el-button>
						</div>
						<!-- 子表格1 -->
						<ypfTable
							v-bind="childTable1Config"
							:tableRows="myInfo.row1.childData1"
							:propList="childTable1Config.originPropList"
							:tableTotal="1"
							@update:tableSimpleSelChange="GetSimpleSelRow1"
						>
							<template #handler="myInfo1">
								<el-button size="small" type="text" @click="childDeleteObjs(myInfo.row1.x1, myInfo1.row1.id, 1)" class="color-red">Delete</el-button>
								<el-upload :show-file-list="false" class="mysub-upload" action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15">
									<el-button size="small" type="text">Upload</el-button>
								</el-upload>
							</template>
						</ypfTable>
					</div>
				</div>
			</template>
</ypfTable>

4.  如何实现表格多级表头的问题?

     propItem.children ,是为了处理多级表头的问题,这里仅仅处理二级表头,    (结合新船系统)

配置文件:  详见 x15_Parent

const contentTableConfig = {
	tableTitle: 'List of Drawings',
	//表格显示列和排列顺序(默认的) ,该对象即使绑定了也不使用, 最终绑定生效的是处理后的对象bindPropList
	originPropList: [
		{ type: 'expand', fixed: 'left', prop: 'spread', label: '', align: 'center', slotName: 'spread' },
		{ prop: 'x13', label: '*Planned Submission Date', align: 'center', width: '110px' },
		{ prop: 'x14', label: 'Iteration Count', align: 'center' },
		{
			prop: 'x15_Parent',
			label: 'EXPERT',
			align: 'center',
			children: [
				{ prop: 'x15', label: 'Received', align: 'center', width: '110px' },
				{ prop: 'x16', label: 'Deadline', align: 'center', width: '110px' },
				{ prop: 'x17', label: 'Sent', align: 'center', width: '110px' },
			],
		},
		{ prop: 'x18', label: 'Approved Drawing', align: 'center' },
		{ prop: 'x19', label: 'Update Approved Drawing', align: 'center' },
	],
	// 开启多选列
	showSelectColumn: false,
	// 是否显示底部分页
	showPagination: false,
};

 

5. 最终代码分享

表格组件 

<template>
    <div class="myContentArea">
        <!-- 1 按钮区域 -->
        <div class="myBtnArea">
            <div><slot name="btnLeft"></slot></div>
            <div><slot name="btnRight"></slot></div>
        </div>
        <!-- 2. 表格区域    -->
        <div><slot name="tableTitle"></slot></div>
        <el-table
            size="default"
            ref="tableRef"
            :[myHeightProp]="`calc(100vh - ${myDynamicHeight})`"
            :data="tableRows"
            v-bind="tableProp"
            @selection-change="tableSelChange"
            @current-change="tableSimpleSelChange"
            @sort-change="sortChange"
            :span-method="mergeFn"
        >
            <el-table-column v-if="showSelectColumn" type="selection" width="26" />
            <template v-for="propItem in propList" :key="propItem.prop">
                <el-table-column v-if="propItem.children" :prop="propItem.prop" :label="propItem.label" :align="propItem.align">
                    <!-- 1. 下面这一层el-table-column 是应对两级表头问题的封装 -->
                    <el-table-column v-for="childPropItem in propItem.children" :key="childPropItem.prop" v-bind="childPropItem">
                        <template #default="scope">
                            <!-- :name,属于具名插槽,有默认值,说明外界可以自己传进来,也可以使用默认值-->
                            <!-- :row1="scope.row" 是插槽prop,对外传值 -->
                            <!-- (1).如果是匿名插槽,外界v-slot:default="myInfo"接收,然后myInfo.row1.xxx调用,然后可以省略为v-slot="myInfo" -->
                            <!-- (2).如果是具名插槽,外界v-slot:name="myInfo"接收, 然后myInfo.row1.xxx调用, 省略为 #name="myInfo",这里的name是实际的propItem.slotName此处是具名插槽! -->
                            <slot :name="childPropItem.slotName" :row1="scope.row">
                                <!-- 下面是默认值,只有当没有提供插入内容的时候才会显示 -->
                                {{ scope.row[childPropItem.prop] }}
                            </slot>
                        </template>
                    </el-table-column>
                </el-table-column>
                <el-table-column v-else v-bind="propItem">
                    <!-- 2. 普通表格,不含多级表头 -->
                    <template #default="scope">
                        <!-- 解释:同上 -->
                        <slot :name="propItem.slotName" :row1="scope.row">
                            <!-- 下面是默认值,只有当没有提供插入内容的时候才会显示 -->
                            {{ scope.row[propItem.prop] }}
                        </slot>
                    </template>
                </el-table-column>
            </template>
        </el-table>

        <!-- 2.3 分页区域 -->
        <el-pagination
            v-if="showPagination"
            style="display: flex; justify-content: center"
            layout="total, sizes, prev, pager, next, jumper"
            class="mt15"
            background
            :page-sizes="[5, 10, 20, 30]"
            :total="tableTotal"
            v-model:current-page="myPage.pageNum"
            v-model:page-size="myPage.pageSize"
        >
        </el-pagination>
    </div>
</template>

<script setup>
import { nextTick, onMounted, reactive, watch } from 'vue';
const store = useStore();

// 1. 接收父组件传递过来的植
const props = defineProps({
    // 表格数据源
    tableRows: {
        type: Array,
        default() {
            return [];
        },
    },
    // 表格总条数
    tableTotal: {
        type: Number,
        default: 0,
    },
    // 表格标题(用处不大)
    tableTitle: {
        type: String,
        default: '',
    },
    // 表格列属性
    propList: {
        type: Array,
        required: true,
    },
    // 表格是否多选
    showSelectColumn: {
        type: Boolean,
        default: false,
    },
    // 是否显示分页
    showPagination: {
        type: Boolean,
        default: false,
    },
    // 分页属性
    page: {
        type: Object,
        default: () => ({ pageNum: 1, pageSize: 10 }),
    },
    // 合并方法
    mergeFn: {
        type: Function,
        default() {},
    },
    // 表格的属性
    tableProp: {
        type: Object,
        default: () => ({}),
    },
    // 是否启用动态设置高度的功能
    isAutoHeight: {
        type: Boolean,
        default: true,
    },
    //绑定的表格动态高度属性,当isAutoHeight=true才生效 (取值有:'height'、'max-height')
    heightProp: {
        type: String,
        default: 'max-height',
    },
});

// 动态决定是否绑定决定表格高度的属性
const myHeightProp = props.isAutoHeight === true ? props.heightProp : '';
const tableRef = ref(null);
let myDynamicHeight = ref('');

/**
 * 计算表格需要减去的高度
 */
const calTableHeight = async () => {
    await nextTick();
    let totalHeight = 160; //默认有tagsView,不全屏
    let tableoffsetTopHeight = tableRef.value.$el.offsetTop; //表格顶部距离tab(tagsView)底部的距离
    let { isTagsview } = store.state.themeConfig.themeConfig;
    if (!isTagsview) {
        totalHeight = 126; // 关闭tagsView,单页面模式
    }
    if (store.state.tagsViewRoutes.isTagsViewCurrenFull) {
        totalHeight = 76; //全屏
    }
    totalHeight += tableoffsetTopHeight;
    myDynamicHeight.value = totalHeight + 'px';
    return myDynamicHeight.value;
};

// 页面加载需要初始化一次
onMounted(() => {
    if (props.isAutoHeight) {
        //此处需要加个延迟,否则第一次加载的时候会计算错误,导致最外层有滚动条
        setTimeout(() => calTableHeight(), 500);

        // 窗口大小变化需要初始化
        window.onresize = () => calTableHeight();
    }
});

/**
 * 监听设置开启or关闭tagsView事件
 * 监听是否全屏
 */
watch([() => store.state.themeConfig.themeConfig.isTagsview, () => store.state.tagsViewRoutes.isTagsViewCurrenFull], () => {
    if (props.isAutoHeight) {
        calTableHeight();
    }
});

// 2. 对外传递事件
const emits = defineEmits(['update:page', 'update:tableSelChange', 'update:tableSimpleSelChange', 'update:sortChange']);

//声明一个新对象用于监听props.page的改变,因为要符合单项数据流,不能直接修改props.page
const myPage = reactive(props.page);

/**
 * 监听分页的改变(包括每页条数 和 页码)
 * @param {Number} newPage 变化后的新的分页对象
 */
watch(myPage, newPage => {
    //  例如:上面通过v-model:current-page进行双向绑定,当点击当前页数变化的时候,myPage中的pageNum会自动改变,pageNum同样道理
    emits('update:page', { ...newPage });
});

/**
 * 监听表格选中行变化(服务于多选)
 * @param {Array} selections 变化后的数组(数组元素为每行的数据)
 */
const tableSelChange = selections => {
    emits('update:tableSelChange', selections);
};

/**
 * 监听表格选中行变化(服务于单选)
 * @param {Object} currentRow 选中行内容
 */
const tableSimpleSelChange = (currentRow, oldCurrentRow) => {
    // console.log(currentRow, oldCurrentRow);
    emits('update:tableSimpleSelChange', currentRow);
};

/**
 * 监听表格排序
 * @param {String} prop 表格排序列名称
 * @param {String} order 表格排序方式 descending 或 ascending
 */
const sortChange = async ({ prop, order }) => {
    let sortName = prop;
    let sortDirection = order == 'ascending' ? 'asc' : 'desc';
    emits('update:sortChange', { sortName, sortDirection });
};

/* 
  对外暴露事件
*/
defineExpose({
    calTableHeight,
});
</script>

<style scoped lang="scss">
.myContentArea {
    background-color: var(--el-color-white);
    padding: 10px 5px 10px 5px;
    .myBtnArea {
        // height: 42px;
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 10px;
    }
    .myBtnArea > div {
        display: flex;
    }
}
</style>
View Code

父页面调用 

<template>
    <div>
        <!-- 1. 搜索栏区域 -->
        <ypfSearch
            :activeName="activeName"
            v-bind="searchFormConfig"
            v-model="formData"
            @update:searchClick="searchClick"
            @update:resetClick="resetClick"
            @update:handleChange="myHandleChange"
        >
            <!-- 此处可以通过具名插槽扩展自己的内容 <template #myOwnArea> </template> -->
        </ypfSearch>

        <!-- 2. 内容区域  -->
        <ypfTable
            ref="ypfTableRef"
            v-bind="contentTableConfig"
            :tableRows="tableData.tableRows"
            :propList="bindPropList"
            :tableTotal="tableData.total"
            :page="tableData.param"
            @update:page="UpdatePage"
            @update:tableSelChange="tableSelChange"
            @update:sortChange="sortChange"
        >
            <!-- 2.1 按钮区域 -->
            <template #btnLeft>
                <el-button size="small" type="success" @click="onOpenAddDialog" v-auth="authList.add"> 新增 </el-button>
                <el-button size="small" type="danger" @click="deleteObjs(null)" v-auth="authList.delMany"> 删除 </el-button>
            </template>
            <template #btnRight>
                <el-button size="small" type="primary" round v-auth="authList.excel" @click="exportExcel"> 导出 </el-button>
                <el-button size="small" type="success" round v-auth="authList.arrange" @click="openSettingDialog"> 设置 </el-button>
            </template>
            <!-- 2.2 表格区域 -->
            <template #userSex="myInfo">
                <el-tag type="success" v-if="myInfo.row1.userSex == 0"></el-tag>
                <el-tag type="info" v-else></el-tag>
            </template>
            <template #addTime="myInfo">
                {{ formatDate(new Date(myInfo.row1.addTime), 'YYYY-mm-dd') }}
            </template>
            <template #handler="myInfo">
                <el-button size="small" type="text" @click="onOpenEditDialog(myInfo.row1)" v-auth="authList.edit">修改</el-button>
                <el-button size="small" type="text" @click="deleteObjs(myInfo.row1.id)" v-auth="authList.delOne">删除</el-button>
            </template>

            <!-- 2.3 分页区域 -->
        </ypfTable>

        <!-- 3. 弹框区域 -->
        <AddUer ref="addDialogRef" @updateTableInfo="initTableData" />
        <EditUser ref="editDialogRef" @updateTableInfo="initTableData" />
        <Setting ref="setDialogRef" @updateTableColumn="initTableColumn" :propList="propList" :pageName="pageName"></Setting>
    </div>
</template>
<script setup name="systemUser">
import { getCurrentInstance, nextTick, onMounted, reactive } from 'vue';
import { useStore } from 'vuex';
import { ElMessage } from 'element-plus';
import AddUer from '/@/views/system/user/component/addUser.vue';
import EditUser from '/@/views/system/user/component/editUser.vue';
import Setting from '/@/components/setting/index.vue';
import { myAxios } from '/@/utils/request';
import { formatDate } from '/@/utils/formatTime';
import { auth } from '/@/utils/authFunction';
import { Session } from '/@/utils/storage';
import ypfTable from '/@/components/ypfTable/index.vue';
import ypfSearch from '/@/components/ypfSearch/index.vue';
import { contentTableConfig } from './config/tableConfig';
import { searchFormConfig } from './config/searchFormConfig';
import { TrimObjStrPros } from '/@/utils/myTools';

const { proxy, type } = getCurrentInstance();
const store = useStore();
const pageName = type.name; //当前页面的name值(systemUser)
const columnLocalKey = `${store.state.userInfos.userInfos.userAccount}_${pageName}`; //表格显示列数据对应的缓存key, eg: admin_systemUser

// 表格对象
const ypfTableRef = ref(null);
/**
 * 监听搜索栏折叠or展开时触发
 */
const myHandleChange = () => {
    // 必须加个延迟,否则获取的高度是折叠前的高度
    setTimeout(() => ypfTableRef.value?.calTableHeight(), 500);
};

// 按钮权限对象
const authList = reactive({
    search: '/system/user/search',
    add: '/system/user/add',
    edit: '/system/user/edit',
    delOne: '/system/user/delOne',
    delMany: '/system/user/delMany',
    excel: '/system/user/excel',
    arrange: '/system/user/arrange',
});
// 绑定组件对象区域
const addDialogRef = ref(null);
const editDialogRef = ref(null);
const setDialogRef = ref(null);
// 表格区域
const tableData = reactive({
    tableRows: [], //表格数据源
    total: 0,
    selectRowIds: [], //表格选中行的id数组
    param: {
        pageNum: 1,
        pageSize: 10,
        sortName: '', //排序字段名称
        sortDirection: '', //排序方式
    },
});
//表格显示列和排列顺序(默认的)
let propList = ref(contentTableConfig.originPropList);
// 绑定用的[...propList.value]
let bindPropList = ref([]);

//搜索栏区域
const activeName = ref('firstCollaspe'); //对应collapse-item的name值,用于默认展开
// 优化:formData中的属性,不需要再写了,完全可以用searchFormConfig配置中的field属性即可
let formOriginData = {};
for (const item of searchFormConfig.formItems) {
    formOriginData[item.field] = '';
    // 下面是处理默认赋值
    if (item.field == 'userSex') {
        formOriginData[item.field] = -1;
    }
}
let formData = ref(formOriginData);

/**
 * 初始化表格数据
 */
const initTableData = async () => {
    if (!auth(authList.search)) {
        ElMessage.error('您没有查询权限');
        return;
    }
    //将string类型的对象去空格
    let myTrimData = TrimObjStrPros(formData.value);
    const { status, data } = await myAxios({
        url: proxy.$url.GetUserInforByConditionUrl,
        data: { ...tableData.param, ...myTrimData },
    });
    if (status == 'ok') {
        tableData.tableRows = data.tableRows;
        tableData.total = data.total;
    }
};
/**
  * 初始化表格显示列和顺序
  * @param {Array} newColumnData 子页面传递过来的表格显示列和顺序
  * 格式: [ { id: '1', label: '账号', columnName: 'userAccount', isSelect: true },
            { id: '2', label: '姓名', columnName: 'userRealName', isSelect: false }    ]
 */
const initTableColumn = async newColumnData => {
    let myColumnData = null;
    if (newColumnData) {
        //一.表示从子页面被动调用
        myColumnData = newColumnData;
    } else {
        // 二. 表示主页面主动加载
        //1. 先从缓存中读取
        myColumnData = Session.get(columnLocalKey);
        //2. 缓存中没有,则请求接口
        if (!myColumnData) {
            const { status, data } = await myAxios({
                url: proxy.$url.GetUserMenuColumnsUrl,
                data: { menuName: pageName },
            });
            if (status == 'ok' && data) {
                myColumnData = data;
                Session.set(columnLocalKey, myColumnData); //存到缓存中
            }
        }
    }
    //3. 处理数据
    if (myColumnData) {
        //3.1 表示从缓存 或 接口 或子页面传递 中拿到了数据
        let latestColumnData = [];
        myColumnData.forEach(item => {
            let newItem = propList.value.find(propItem => propItem.prop === item.columnName && item.isSelect == true);
            if (newItem) latestColumnData.push(newItem);
        });
        bindPropList.value = []; //必须先清空一下,才会触发数据改变
        nextTick(() => {
            bindPropList.value = [...latestColumnData];
        });
    } else {
        //3.2 表示缓存或者接口中都没有数据,直接用前端都默认值赋值
        nextTick(() => {
            bindPropList.value = [...propList.value];
        });
    }
};
/**
 * 搜索事件
 */
const searchClick = async () => {
    // console.log(formData.value);
    await initTableData();
};
/**
 * 重置事件
 */
const resetClick = async () => {
    // 清空数据
    Object.keys(formData.value).forEach(key => {
        formData.value[key] = '';
        // 下面是处理默认赋值问题
        if (key == 'userSex') {
            formData.value[key] = -1;
        }
    });
    await initTableData();
};

// 打开新增弹窗
const onOpenAddDialog = () => {
    addDialogRef.value.openDialog();
};
/**
 * 打开修改弹窗
 * @param {Object} row:该行数据
 */
const onOpenEditDialog = row => {
    editDialogRef.value.openDialog(row);
};
/**
 * 打开设置弹框
 */
const openSettingDialog = () => {
    setDialogRef.value.openDialog();
};

/**
 * 删除(单个 和 多个)
 * @param {String} row 单个删除时,删除的id
 * 对于多个删除,deleteId 为 null
 */
const deleteObjs = async deleteId => {
    let delIdsStr = deleteId ?? tableData.selectRowIds;
    if (delIdsStr.length === 0) {
        proxy.$message('请选择要删除的行');
        return;
    }
    let confirmResult = await proxy.$myConfirm('您确定要删除该账户吗?');
    if (confirmResult == 'confirm') {
        const { status, msg } = await myAxios({
            url: proxy.$url.DelUserUrl,
            data: { delIdsStr },
        });
        if (status === 'ok') {
            ElMessage.success(msg);
            await initTableData();
        }
    }
};

/**
 * 监听表格选中行变化
 * @param {Array} selections 变化后的数组(数组元素为每行的数据)
 */
const tableSelChange = selections => {
    let selectRowIds = selections.map(item => item.id); //返回一个id集合数组
    tableData.selectRowIds = selectRowIds;
};

/**
 * 监听子页面传递过来的表格排序参数
 * @param {Object}  pageInfo {sortName: xx, sortDirection: xx}
 * 解释:sortName 表格排序列名称, sortDirection 表格排序方式
 */
const sortChange = async ({ sortName, sortDirection }) => {
    if (sortName && sortDirection) {
        tableData.param.sortName = sortName;
        tableData.param.sortDirection = sortDirection;
        await initTableData();
    }
};

/**
 * 监听子页面传递的表格分页参数
 * @param {Object} pageInfo {pageNum: xx, pageSize: xx}
 */
const UpdatePage = pageInfo => {
    if (pageInfo) {
        //tableData.param 中的sortDirection和sortName被置为 "", 不影响业务
        tableData.param = { ...pageInfo };
        initTableData();
    }
};

/**
 * 按顺序导出显示列的excel表格
 */
const exportExcel = async () => {
    let confirmResult = await proxy.$myConfirm('您确定要导出Excel吗?');
    let myTrimData = TrimObjStrPros(formData.value); //将string类型的对象去空格
    let searchStr = JSON.stringify(myTrimData); //搜索条件字符串
    let pageStr = JSON.stringify(tableData.param); //分页条件字符串
    //需要导出列的信息  注:只有含有excelWidth属性的列才是需要导出的列
    let bindPropListStr = JSON.stringify(bindPropList.value.filter(item => item.excelWidth));
    if (confirmResult == 'confirm') {
        // 版本1-路径的形式
        // window.location.href = proxy.$url.UserDownLoadFileExcelUrl + '?auth=' + Session.get('token') + '&searchStr=' + searchStr + '&pageStr=' + pageStr + '&bindPropListStr=' + bindPropListStr;

        // 版本2-文件流版本
        await myAxios({
            url: proxy.$url.UserDownLoadFileExcelUrl2,
            responseType: 'blob',
            data: { searchStr, pageStr, bindPropListStr },
        });
    }
};

// 页面实例挂载完成后
onMounted(async () => {
    // 1. 初始化表格显示列和排列顺序
    await initTableColumn();
    // 2. 初始化表格数据
    await initTableData();
});
</script>
View Code

配置代码

const contentTableConfig = {
    tableTitle: '用户管理',
    //表格显示列和排列顺序(默认的) , 该对象即使绑定了也不使用, 最终绑定生效的是处理后的对象bindPropList
    originPropList: [
        { prop: 'userAccount', label: '账号', align: 'center', sortable: 'custom', width: '150', slotName: 'userAccount', excelWidth: 20 },
        { prop: 'userRealName', label: '姓名', align: 'center', width: '150', slotName: 'userRealName', excelWidth: 20 },
        { prop: 'userSex', label: '性别', align: 'center', width: '100', slotName: 'userSex', excelWidth: 20 },
        { prop: 'userPhone', label: '联系方式', align: 'center', width: '200', slotName: 'userPhone', excelWidth: 50 },
        { prop: 'userRemark', label: '用户描述', align: 'center', slotName: 'userRemark', 'show-overflow-tooltip': true, excelWidth: 20 },
        { prop: 'addTime', label: '创建时间', align: 'center', sortable: 'custom', slotName: 'addTime', excelWidth: 40 },
        { prop: 'handler', label: '操作', align: 'center', width: '100', slotName: 'handler' },
        // { prop: 'handler2', label: '操作2', align: 'center', width: '100', slotName: 'handler' },
    ],
    // 开启多选列 【启用多选下面的'highlight-current-row',必须设为false,或者不写这个属性】
    showSelectColumn: true,
    // 是否显示底部分页
    showPagination: true,
    // 是否开启表格高度自适应
    isAutoHeight: true,
    // 表格高度自适应模式,当isAutoHeight=true才生效 (取值有:'height'、'max-height'), 默认值为:'max-height'
    heightProp: 'height',
    // 表格属性
    tableProp: {
        'highlight-current-row': false, //设置单选,启用单选必须将上述的showSelectColumn改为false
    },
};

export { contentTableConfig };
View Code

 

三.  分页组件3种传值方式 【子传父】

1.   v-model绑定 + watch监听 【首选方案】

   使用v-model 双向绑定current-page、page-size属性实现变化的同时进行赋值,比如:v-model:current-page="myPage.pageNum"  只需要通过watch对象监听myPage属性的变化,然后对外emit暴露即可【详见  components/ypfTable/index.vue】
 
template代码 
<el-pagination
    v-if="showPagination"
    style="display: flex; justify-content: center"
    layout="total, sizes, prev, pager, next, jumper"
    class="mt15"
    background
    :page-sizes="[5, 10, 20, 30]"
    :total="tableTotal"
    v-model:current-page="myPage.pageNum"
    v-model:page-size="myPage.pageSize"
>
</el-pagination>
script代码 
<script setup>
// 1. 接收父组件传递过来的植
const props = defineProps({
	// 分页属性
	page: {
		type: Object,
		default: () => ({ pageNum: 1, pageSize: 10 }),
	},
});
// 2. 对外传递事件
const emits = defineEmits(['update:page']);

//3. 声明一个新对象用于监听props.page的改变,因为要符合单项数据流,不能直接修改props.page
const myPage = reactive(props.page);

/**
 * 4. 监听分页的改变(包括每页条数 和 页码)
 * @param {Number} newPage 变化后的新的分页对象
 */
watch(myPage, newPage => {
	//  例如:上面通过v-model:current-page进行双向绑定,当点击当前页数变化的时候,myPage中的pageNum会自动改变,pageNum同样道理
	emits('update:page', { ...newPage });
});
    
</script>
 

2.  v-model双向绑定 + 监听size-change和current-change事件  【不推荐】

    使用v-model 双向绑定current-page、page-size属性,同时两个方法监听@size-change="onPageChange" @current-change="onPageChange"    在监听的函数中调用 emit对外暴露  【components/ypfTable/index_v2.vue 】

template代码 

<el-pagination
    v-if="showPagination"
    style="display: flex; justify-content: center"
    layout="total, sizes, prev, pager, next, jumper"
    class="mt15"
    background
    :page-sizes="[5, 10, 20, 30]"
    :total="tableTotal"
    v-model:current-page="myPage.pageNum"
    v-model:page-size="myPage.pageSize"
    @size-change="onPageChange"
    @current-change="onPageChange"
>
</el-pagination>

script代码 

<script setup>
// 1. 接收父组件传递过来的植
const props = defineProps({
	// 分页属性
	page: {
		type: Object,
		default: () => ({ pageNum: 1, pageSize: 10 }),
	},
});
// 2. 对外传递事件
const emits = defineEmits(['update:page']);

//3. 声明一个新对象用于监听props.page的改变,因为要符合单项数据流,不能直接修改props.page
const myPage = reactive(props.page);

/**
 * 4. 监听分页的改变(包括每页条数 和 页码)
 * @param {Number} newPageSize 新的分页条数
 */
const onPageChange = () => {
	//  例如:上面通过v-model:current-page进行双向绑定,当点击当前页数变化的时候,page中的pageNum会自动改变, 实际上已经不需要这里返回的参数了
	emits('update:page', { ...myPage });
};
    
</script>

 

3.  current-page和page-size绑定 + 监听size-change和current-change事件  【不推荐】

     通过 :current-page="myPage.pageNum",:page-size="myPage.pageSize"绑定,然后@size-change和@current-change对应不同的监听函数,通过监听函数传递的参数拿到最新的页码 或 每页的条数,然后对外暴露 【麻烦,不推荐】

 

总结: 肯定推荐方案1,另外方案1和方案2,均不影响父页面的写法

 

 

四.  父页面_调用封装中的分页组件 【父调子】

 1.  page单向传值 + @update:page监听 【推荐】

    使用  :page="tableData.param"传值,然后使用  @update:page="UpdatePage"监听,然后在UpdatePage方法中给tableData.param赋最新值,如下:【详见user/index.vue】

template中代码:

<ypfTable
    ref="ypfTableRef"
    v-bind="contentTableConfig"
    :tableRows="tableData.tableRows"
    :propList="bindPropList"
    :tableTotal="tableData.total"
    :page="tableData.param"
    @update:page="UpdatePage"
    @update:tableSelChange="tableSelChange"
    @update:sortChange="sortChange"
>
</ypfTable>

script中代码: 

<script setup>
const tableData = reactive({
	param: {
		pageNum: 1,
		pageSize: 10,
		sortName: '', //排序字段名称
		sortDirection: '', //排序方式
	},
});
/**
 * 监听子页面传递的表格分页参数
 * @param {Object} pageInfo {pageNum: xx, pageSize: xx}
 */
const UpdatePage = pageInfo => {
	if (pageInfo) {
		//tableData.param 中的sortDirection和sortName被置为 "", 不影响业务
		tableData.param = { ...pageInfo };
		initTableData();
	}
};

</script>

 

2. v-mode双向绑定page + watch监听属性pageNum和pageSize 【更推荐--因为写法更简洁】

  直接使用语法糖  v-model:page="tableData.param"传值的同时,接收到子页面传过来值的时候自动赋值给table.param,然后使用watch监听 tableData.param即可。

  特别注意:这里tableData.param是一个reactive对象的属性,所以要监听的时候要这样写:  ()=>tableData.param,但是param中还有排序相关的两个属性(sortName和sortDirection),直接监听param的话会和点击列排序实际同时调用,导致加载了两次数据,所以最终监听直接监听这两个属性,如下:【详见user/v2_index.vue】

template中代码: 

<ypfTable
    ref="ypfTableRef"
    v-bind="contentTableConfig"
    :tableRows="tableData.tableRows"
    :propList="bindPropList"
    :tableTotal="tableData.total"
	v-model:page="tableData.param"
    @update:tableSelChange="tableSelChange"
    @update:sortChange="sortChange"
>
</ypfTable>

 script中代码:  

<script setup>
const tableData = reactive({
	param: {
		pageNum: 1,
		pageSize: 10,
		sortName: '', //排序字段名称
		sortDirection: '', //排序方式
	},
});
/**
 * 监听分页参数的变化
 * param中还有排序相关的两个属性,直接监听param的话回和点击列排序实际同时调用,导致加载了两次数据,所以最终监听直接监听这两个属性
 */
watch([() => tableData.param.pageNum, () => tableData.param.pageSize], () => {
	console.log(tableData.param);
	initTableData();
});


</script>

 

五.  表格高度自适应方案

1. 原理

(1). 核心点就是动态给el-table组件赋值,修改height属性 max-height属性(二者效果是有区别的)

      height:表示表格永远占满屏幕

      maxHeight:当数据量小的时候,表格还是实际高度,只有超过屏幕,才会占满屏幕,出现滚动条

(2). 在ypf-table组件中:

     A. 使用isAutoHeight接收传递过来的值,从而决定是否开启动态计算高度默认是开启的;即绑定一个空字符串属性,是无效的,若开启,则绑定heightProp ,即  max-height 或者 height 属性。

     B. 使用 heightProp 接收传递过来的值,从而决定表格的高度模式采用: max-height 还是 height 属性, 注:默认是max-height,即可以不传递这个属性

  

// 1. 接收父组件传递过来的植
const props = defineProps({
	// 是否启用动态设置高度的功能
	isAutoHeight: {
		type: Boolean,
		default: true,
	},
	//绑定的表格动态高度属性,当isAutoHeight=true才生效 (取值有:'height'、'max-height')
	heightProp: {
		type: String,
		default: 'max-height',
	},
});

// 动态决定是否绑定决定表格高度的属性
const myHeightProp = props.isAutoHeight === true ? props.heightProp : '';

 

(3). 如何计算表格的高度?   (表格高度不包括分页条,不含上面的按钮区域)

      a.  这里采用减法calc(100vh - ${myDynamicHeight}),100vh代表屏幕高度,myDynamicHeight 代表其它非表格位置占的的高度

      b.  封装calTableHeight方法,计算非表格位置的高度,这里需要考虑三种情况:默认有tagsView、关闭tagsView、全屏

      c.  核心点是使用offsetTop,计算表格顶部距离tab(tagsView)底部的距离,这样就无需分情况计算搜索框的高度了,简化了很多步骤

      d. 调用位置:onMounted的时候默认初始化、监听窗口变化、监听是否开启or关闭tagsView

      e. 关于calTableHeight方法的几点说明:

        ① tableoffsetTopHeight:代表表格顶部距离tab(tagsView)底部的距离

        ② totalHeight= 整个系统的屏幕高度 - 表格高度 - tableoffsetTopHeight   (这是一个反向说明)

        ③ 全屏的时候  : totalHeight=76px

        ④ 关闭tagView,单页面形式 : totalHeight=76px+50px=126px

        ⑤ 默认tagView,不全屏:totalHeight=76px+50px+34px=160px

(4). ypf-search组件中需要监听一下handleChange是否折叠 or 展开的变化,对外暴露,在最外层调用页面中,获取变化的时候,重新计算表格高度。

template代码:

<el-table
    size="default"
    ref="tableRef"
    :[myHeightProp]="`calc(100vh - ${myDynamicHeight})`"
    :data="tableRows"
    v-bind="tableProp"
    @selection-change="tableSelChange"
    @current-change="tableSimpleSelChange"
    @sort-change="sortChange"
    :span-method="mergeFn"
>
   xxxx
</el-table>

script代码 

<script setup>

// 1. 接收父组件传递过来的植
const props = defineProps({
	// 是否启用动态设置高度的功能
	isAutoHeight: {
		type: Boolean,
		default: true,
	},
	//绑定的表格动态高度属性,当isAutoHeight=true才生效 (取值有:'height'、'max-height')
	heightProp: {
		type: String,
		default: 'max-height',
	},
});

// 动态决定是否绑定决定表格高度的属性
const myHeightProp = props.isAutoHeight === true ? props.heightProp : '';
const tableRef = ref(null);
let myDynamicHeight = ref('');

/**
 * 计算表格需要减去的高度
 */
const calTableHeight = async () => {
	await nextTick();
	let totalHeight = 160; //默认有tagsView,不全屏
	let tableoffsetTopHeight = tableRef.value.$el.offsetTop; //表格顶部距离tab(tagsView)底部的距离
	let { isTagsview } = store.state.themeConfig.themeConfig;
	if (!isTagsview) {
		totalHeight = 126; // 关闭tagsView,单页面模式
	}
	if (store.state.tagsViewRoutes.isTagsViewCurrenFull) {
		totalHeight = 76; //全屏
	}
	totalHeight += tableoffsetTopHeight;
	myDynamicHeight.value = totalHeight + 'px';
	return myDynamicHeight.value;
};

// 页面加载需要初始化一次
onMounted(() => {
	if (props.isAutoHeight) {
		//此处需要加个延迟,否则第一次加载的时候会计算错误,导致最外层有滚动条
		setTimeout(() => calTableHeight(), 500);

		// 窗口大小变化需要初始化
		window.onresize = () => calTableHeight();
	}
});

/**
 * 监听设置开启or关闭tagsView事件
 * 监听是否全屏
 */
watch([() => store.state.themeConfig.themeConfig.isTagsview, () => store.state.tagsViewRoutes.isTagsViewCurrenFull], () => {
	if (props.isAutoHeight) {
		calTableHeight();
	}
});

/* 
  对外暴露事件
*/
defineExpose({
	calTableHeight,
});

</script>

 

2.  如何使用  

(1). 表格的配置文件tableConfig.js 中需要添加

    A. isAutoHeight:表示开启表格高度自适应

    B.  heightProp:表示表格高度自适应的模式

 注:这两个属性在表格封装中均有默认值,所以可以根据实际情况省略。

const contentTableConfig = {
	// 是否开启表格高度自适应
	isAutoHeight: true,
	// 表格高度自适应模式,当isAutoHeight=true才生效 (取值有:'height'、'max-height'), 默认值为:'max-height'
	heightProp: 'height',
};

(2). 在使用ypf-table组件的时候,通过v-bind绑定属性即可。 

 

(3). 如果主页面中使用了ypf-search组件,需要监听  @update:handleChange="myHandleChange",在myHandleChange方法中,调用ypf-table中的计算高度的方法

/**
 * 监听搜索栏折叠or展开时触发
 */
const myHandleChange = () => {
	// 必须加个延迟,否则获取的高度是折叠前的高度
	setTimeout(() => ypfTableRef.value?.calTableHeight(), 500);
};

 

六.  其它功能剖析

1. 表格列排序

   需将给table-column列上的 sortable 设置为 custom,同时在 Table 上监听 sort-change 事件, 在事件回调中可以获取当前排序的字段名和排序顺序,从而向接口请求排序后的表格数据。

(1). 数据准备  

   账号和创建时间两列,配置 sortable: 'custom',支持列排序。

const contentTableConfig = {
	//表格显示列和排列顺序(默认的) , 该对象即使绑定了也不使用, 最终绑定生效的是处理后的对象bindPropList
	originPropList: [
		{ prop: 'userAccount', label: '账号', align: 'center', sortable: 'custom', width: '150', slotName: 'userAccount', excelWidth: 20 },
		{ prop: 'userRealName', label: '姓名', align: 'center', width: '150', slotName: 'userRealName', excelWidth: 20 },
		{ prop: 'userSex', label: '性别', align: 'center', width: '100', slotName: 'userSex', excelWidth: 20 },
		{ prop: 'userPhone', label: '联系方式', align: 'center', width: '200', slotName: 'userPhone', excelWidth: 50 },
		{ prop: 'userRemark', label: '用户描述', align: 'center', slotName: 'userRemark', 'show-overflow-tooltip': true, excelWidth: 20 },
		{ prop: 'addTime', label: '创建时间', align: 'center', sortable: 'custom', slotName: 'addTime', excelWidth: 40 },
		{ prop: 'handler', label: '操作', align: 'center', width: '100', slotName: 'handler' },
	],
};

(2). 组件封装

    监听sort-change事件,然后对外传递事件和参数。

<script setup>

// 1. 对外传递事件
const emits = defineEmits([ 'update:sortChange']);

/**
 * 2. 监听表格排序
 * @param {String} prop 表格排序列名称
 * @param {String} order 表格排序方式 descending 或 ascending
 */
const sortChange = async ({ prop, order }) => {
	let sortName = prop;
	let sortDirection = order == 'ascending' ? 'asc' : 'desc';
	emits('update:sortChange', { sortName, sortDirection });
};

</script>

(3). 父页面调用

     通过@update:sortChange获取子页面传递,然后接收参数,调用表格加载方法 initTableData();

/**
 * 监听子页面传递过来的表格排序参数
 * @param {Object}  pageInfo {sortName: xx, sortDirection: xx}
 * 解释:sortName 表格排序列名称, sortDirection 表格排序方式
 */
const sortChange = async ({ sortName, sortDirection }) => {
	if (sortName && sortDirection) {
		tableData.param.sortName = sortName;
		tableData.param.sortDirection = sortDirection;
		await initTableData();
	}
};

 

2. 表格行多选

    原理很简单,在table组件内部, 手动添加一个 el-table-column,设 type 属性为 selection 即可。 这里通过 v-if="showSelectColumn",控制是否显示多选这里一列。然后监听 selection-change 事件,获取选中的行,最后通过emit向父组件传递。

注:必须关掉单选,即 'highlight-current-row': false  

(1). 数据准备  

const contentTableConfig = {
	// 开启多选列 【启用多选下面的'highlight-current-row',必须设为false,或者不写这个属性】
	showSelectColumn: true,
	// 表格属性
	tableProp: {
		'highlight-current-row': false, //设置单选,启用单选必须将上述的showSelectColumn改为false
	},
};

(2). 组件封装    

 

// 1. 接收父组件传递过来的值
const props = defineProps({
	// 表格是否多选
	showSelectColumn: {
		type: Boolean,
		default: false,
	},
});

// 2. 对外传递事件
const emits = defineEmits(['update:tableSelChange']);

/**
 * 3. 监听表格选中行变化(服务于多选)
 * @param {Array} selections 变化后的数组(数组元素为每行的数据)
 */
const tableSelChange = selections => {
	emits('update:tableSelChange', selections);
};

(3). 父页面调用

 

/**
 * 监听表格选中行变化
 * @param {Array} selections 变化后的数组(数组元素为每行的数据)
 */
const tableSelChange = selections => {
	let selectRowIds = selections.map(item => item.id); //返回一个id集合数组
	tableData.selectRowIds = selectRowIds;
};

 

3. 表格行单选

   Table 组件提供了单选的支持, 只需要配置 highlight-current-row 属性(高亮显示)即可实现单选。 之后由 current-change 事件来管理选中时触发的事件,它会传入 currentRowoldCurrentRow

   注:这里的单选和上面的多选不能同时配置,考虑如何将多选的模式改为单选,因为现在的单选,只能高亮选中行,没有checkbox。

(1). 数据准备  

const contentTableConfig = {
	// 开启多选列 【启用多选下面的'highlight-current-row',必须设为false,或者不写这个属性】
	showSelectColumn: false,
	// 是否开启表格高度自适应
	isAutoHeight: true,
	// 表格属性
	tableProp: {
		'highlight-current-row': true, //设置单选,启用单选必须将上述的showSelectColumn改为false
	},
};

(2). 组件封装  

// 1. 接收父组件传递过来的植
const props = defineProps({
	// 表格的属性
	tableProp: {
		type: Object,
		default: () => ({}),
	},
});

// 2. 对外传递事件
const emits = defineEmits(['update:tableSimpleSelChange']);

/**
 * 3. 监听表格选中行变化(服务于单选)
 * @param {Object} currentRow 选中行内容
 */
const tableSimpleSelChange = (currentRow, oldCurrentRow) => {
	// console.log(currentRow, oldCurrentRow);
	emits('update:tableSimpleSelChange', currentRow);
};

(3). 父页面调用 

<ypfTable
			ref="ypfTableRef"
			v-bind="contentTableConfig"
			:tableRows="tableData.tableRows"
			:propList="bindPropList"
			:tableTotal="tableData.total"
			:page="tableData.param"
			@update:page="UpdatePage"
			@update:tableSimpleSelChange="tableSimpleSelChange"
			@update:sortChange="sortChange"
>
                
/**
 * 监听表格选中行变化
 * @param {Object} selRow  选中行
 */
const tableSimpleSelChange = selRow => {
        //相关业务
};             
                

4. 支持列合并、行合并的方法 

   调用的时候,传递进来自定义方法即可。

/**
 * 合并单元格
 * @param {int} rowIndex 当前行号
 * @param {int} columnIndex 当前列号
 */
const objectSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
	// 当前列号等于0,表示只有第一列合并
	if (columnIndex === 0) {
		if (rowIndex % 3 === 0) {
			return { rowspan: 3, colspan: 1 };
		} else {
			return { rowspan: 0, colspan: 0 };
		}
	}
};

 

 

七.  持续更新...

 

   (1) 去掉了表格封装中搜索栏默认高度,从而实现不传入btn内容的时候,不占位置

   (2) 修改表格封装,支出表头合并的情况,注:仅仅支持1级合并[带来了一个新问题:表格默认就有竖直方向的边框了,可以接受,默认就是这样的]

   (3) 封装了支持列合并、行合并的方法,至于如何合并,自己编写方法,传递进去即可

   (4) 新增了tableProp,用来传递table属性

   (5) 删掉了表格上默认样式: style="width: 100%"

   (6) 表格上方左右两侧的按钮myBtnArea区域中的div增加flex布局

   (7) 新增监听单选表格单选事件,尚不完善,考虑通过个属性来控制是否监听,而不是全部监听 【尚不完善】

   (8) 关于多选的监听,目前是在封装中处理,把id返回来了,这个在一些场景中并不是很实用,改为:对外传递整行数据,在调用页面处理业务

   (9) css样式的padding和margin都改了

 

 

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2023-01-04 15:18  Yaopengfei  阅读(197)  评论(1编辑  收藏  举报