el-table 使用总结
实现树形数据与懒加载、列字段排序、表头固定、详情路由跳转、后端分页
添加属性:
渲染树形数据时,必须要指定 row-key
row-key=“id”
通过指定 row 中的 hasChildren 字段来指定哪些行是包含子节点。children 与 hasChildren 通过 tree-props 配置
:tree-props="{children: ‘children’, hasChildren: ‘hasChildren’}"
row-class-name 属性来为 Table 中的某一行添加 class,本例用于设置父子内容背景色
:row-class-name=“tableRowBgColor”
设置父子内容背景色:
tableRowBgColor ({row}) {
if (row.hasChildren) {
return 'has-children-row'
}
if (row.isChildren) {
return 'is-children-row'
}
return ''
}
单元格内容换行:
<el-table-column
:label="$t('loc.assignedTo')"
width="120">
<template slot-scope="scope">
<span>{{$t('loc.recipients')}} ({{scope.row.sharedUsers.length}})span>
<br/>
<span class="overflow-ellipsis w-full" :title="scope.row.sharedUserNames">{{scope.row.sharedUserNames}}span>
template>
el-table-column>
/deep/ .creator-col > .cell {
display: flex;
align-items: center;
.el-table__expand-icon {
flex: none;
}
.creator-name {
flex: auto;
min-width: 0;
}
}
.overflow-ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
解决 el-table-column 限宽,列内容与表头错位问题:
/deep/ .el-table table{
width: 0px;
}
添加属性:
设置 Table 的 lazy 属性为 true 与加载函数 load
lazy
:load=“getSharedRecordPlans”
懒加载列:
<el-table-column
:label="$t('loc.assignedBy2')"
sortable="custom"
width="120"
class-name="creator-col"
:column-key="'CREATE_SHARE_USER'">
<template slot-scope="scope">
<div class="overflow-ellipsis creator-name" :title="scope.row.createUsername">{{scope.row.createUsername}}div>
template>
el-table-column>
加载子节点数据的函数:
resolve() 中传入子节点数据
methods: {
...
getSharedRecordPlans (tree, treeNode, resolve) {
let params = {
shareId: tree.id,
pageSize: 50,
}
this.$axios.post($api.urls().listSharedRecordPlans, params)
.then(response => {
// 分享记录下周计划列表
let childrenItems = []
if (response.items.length > 0) {
// 向子记录中添加父记录内容
response.items.map((item) => {
childrenItems.push(Object.assign({}, item, {
'shareUser': tree.shareUser,
'planCount': 1,
'sharedAtUtc': tree.sharedAtUtc,
'sharedUsers': tree.sharedUsers,
'sharedUserNames': tree.sharedUserNames,
'comment': tree.comment,
'isChildren': true
}))
})
}
resolve(childrenItems)
}).catch(error => {})
},
...
}
添加属性:
列中设置 sortable 属性,通过 Table 的default-sort属性设置默认的排序列和排序顺序。可以使用sort-method或者sort-by使用自定义的排序规则。
如果需要后端排序,需将sortable设置为custom,同时在 Table 上监听sort-change事件
@sort-change=“sortChange”
本例采用后端排序
html 代码
:column-key="‘SHARE_USER’" 设置对应后端枚举值
data () {
return {
...
sortField: '', //拍序列
sortOrder: '', //排序规则
...
}
}
methods: {
...
sortChange (params) {
let order = params.order
if (!order) {
this.sortField = undefined
this.sortOrder = undefined
} else {
let columnKey = params.column.columnKey
this.sortField = columnKey ? columnKey : params.column.label
this.sortOrder = params.order === 'ascending' ? 'ASC' : 'DESC'
}
// 刷新数据
this.loadData()
}
...
loadData () {
...
// 选择了排序,则按照字段排序
if (this.sortField && this.sortOrder) {
params['orderKey'] = this.sortField
params['orderType'] = this.sortOrder
}
...
this.loader(params).then(data => {
...
}).catch(error => {
...
})
}
...
}
loadData
TeacherSharedTable.vue
<template>
...
<div>
<shared-plan-table ref="sharededPlanTable" :loader="listSharedPlans">shared-plan-table>
div>
...
templete>
<script>
import LessonApi from '@/api/lessons2'
import SharedPlanTable from './components/SharedPlanTable'
export default {
name: 'SharedPlanList',
components: {SharedPlanTable},
...
methods: {
listSharedPlans (params) {
params['shareUserId'] = this.shareUserId
return LessonApi.listSharedPlans(params)
...
}
}
}
script>
添加属性:
只要在el-table元素中定义了height属性,即可实现固定表头
:height=“tableHeight”
原始方案:
新问题:合适的 tableHeight
ofCourse el-table 中添加 ref=“table”
SharePlanTable.vue
created () {
...
this.calcTableHeight ()
...
}
methods: {
...
// 计算表格高度
calcTableHeight () {
this.$nextTick(function () {
this.tableHeight = window.innerHeight - this.$refs.table.$el.offsetTop - 50;
// console.log("aaa", this.tableHeight)
// 监听窗口大小变化
let self = this;
window.onresize = function() {
self.tableHeight = window.innerHeight - self.$refs.table.$el.offsetTop - 50
}
})
}
...
}
莫名奇妙的问题, 在一个页面回退时,方法正常调用, tableHeight = 0
新方案:
<template>
<div class="admin-shared-table display-flex flex-direction-col h-full">
<div>
<el-table ...>
eltable>
div>
div>
<div class="table-footer flex-none">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[10, 20, 30]"
:page-size="pageSize"
layout="total, sizes, ->, prev, pager, next"
:total="total">
el-pagination>
div>
.display-flex {
display: -webkit-flex;
display: flex;
}
.flex-direction-col {
flex-direction: column
}
.flex-none {
flex: none;
}
.flex-auto {
flex: auto;
min-width: 0;
}
原理:
纵向 flex
表格 div auto 拓展 !!!el-table 设置 height=“100%”
分页 div none 自身撑开高度
flex布局中flex:1和flex:auto的区别
打开率跳转列:
<el-table-column
:label="$t('loc.openRate')"
width="90">
<template slot-scope="scope">
<el-button
size="medium"
type="text"
:disabled="scope.row.planCount > 1 || scope.row.planCount === 0"
@click="viewOpenRate(scope.row.shareId, scope.row.planId)"><span>{{scope.row.percent}}span>
el-button>
template>
el-table-column>
父级记录条打开率跳转按钮禁用,但禁用样式不美观
修改按钮禁用样式:
/deep/ .el-button.is-disabled {
// 恢复 ElementUI 黑色字体色号
color: #606266 !important;
// 恢复透明度
opacity: 1 !important;
}
路由跳转:
methods: {
...
// 查看周计划 Open Rate 跳转
viewOpenRate (shareId, planId) {
this.$router.push({
name: 'view-plan',
params: {
planId: planId
},
query: {
shareId: shareId,
param: 'openRate'
}
})
}
...
}
src/router/modules/lesson2.js
// lesson Plan
{
path: 'weekly-lesson-planning',
component: () => import('@/views/modules/lesson2/Layout.vue'),
children: [
{
path: '',
name: 'plan',
component: () => import('@/views/modules/lesson2/lessonPlan/WeeklyPlan.vue'),
children: [
{
path: 'list',
name: 'list-plan',
component: () => import('@/views/modules/lesson2/lessonPlan/PlanList.vue')
},
{
// path: 'assignedPlan/:adminId',
path: 'create-virtual-shadow-list',
name: 'assigned-plan',
component: () => import('@/views/modules/lesson2/lessonPlan/AssignedPlanList.vue')
},
{
path: 'shadowed-list',
name: 'teacherShadow',
component: () => import('@/views/modules/lesson2/lessonPlan/TeacherShadowedList.vue')
}
]
},
{
path: 'edit/:planId',
name: 'edit-plan',
component: () => import('@/views/modules/lesson2/lessonPlan/EditPlan.vue')
},
{
path: 'view/:planId',
name: 'view-plan',
component: () => import('@/views/modules/lesson2/lessonPlan/ViewPlan.vue')
}
...
]
}
路径 url:
http://localhost:8080/#/lessons/weekly-lesson-planning/view/EBEE2C19-1031-4B48-82DF-3BEDF8EE61C5?shareId=ECFA84C8-FE79-4237-9313-454589D18503¶m=openRate
params planId -> /:planId
query shareId … ->?shareId=
vue 中的动态传参和query传参:
Vue router 如何传参
params:/router1/:id,这里的 id 叫做 params。例如/router1/123, /router1/789
接收参数:this.$route.params.idquery:/router1?id=123,这里的 id 叫做 query。例如/router1?id=456
接收参数: this.$route.query.id
created () {
// 获取周计划 ID
this.planId = this.$route.params.planId
}
methods: {
...
handleSizeChange (size) {
this.pageSize = size
this.currentPage = 1
// 刷新数据
this.loadData()
},
handleCurrentChange (page) {
this.currentPage = page
// 刷新数据
this.loadData()
}
...
}
后台代码:
planServiceImpl.java
...
/**
* 获取分享记录列表
*
* @param request 获取分享记录列表请求信息
* @return 分享记录列表响应信息
*/
@Override
public ListSharedRecordsResponse listSharedRecords(ListSharedRecordsRequest request) {
...
// 检查参数
this.checkListParam(request);
...
// 获取这些老师被分享的记录
PageList<ShareEntity> shareEntitiesPage = planShareDao.listByShareUserIds(request.getPageSize(),
request.getPage(), request.getOrderKey(), request.getOrderType(), user.getAgencyId(), teacherIds);
...
}
...
/**
* 检查分页、排序参数
* @param request 请求信息
*/
private void checkListParam(ListSharedRecordsRequest request) {
// 页码不能小于 1,默认第 1 页
if (request.getPage() < 1) {
request.setPage(1);
}
// 每页条数不能小于 1,默认 1 页 10 条记录
if (request.getPageSize() < 1) {
request.setPageSize(10);
}
// 每页条数不能大于 50
if (request.getPageSize() > 50) {
request.setPageSize(50);
}
// 解析排序字段
PlanOrderKey planOrderKey = PlanOrderKey.parse(request.getOrderKey());
// 设置排序字段
request.setOrderKey(planOrderKey.toString());
// 解析排序类型
OrderType orderType = OrderType.parse(request.getOrderType());
// 设置排序类型
request.setOrderType(orderType.toString());
}
...
planOrderKey.java
package com.learninggenie.common.data.enums.lesson2;
public enum PlanOrderKey {
TIME, // 时间
STATUS, // 状态
CREATE_SHARE_USER, // 创建分享人
SHARE_USER; // 被分享人
/**
* 解析枚举类型字符串
* @param key 枚举类型字符串
* @return 枚举对象
*/
public static PlanOrderKey parse(String key) {
// 过滤空值
if (key == null) {
return TIME;
}
// 遍历枚举对象
for (PlanOrderKey orderKey : PlanOrderKey.values()) {
// 如果和要解析的名称一致,则返回
if (orderKey.toString().equalsIgnoreCase(key.trim())) {
return orderKey;
}
}
// 没有匹配的,则默认按照时间排序
return TIME;
}
}
PlanShareDaoImpl.java
@Repository
public class PlanShareDaoImpl extends ServiceImpl<PlanShareMapper, ShareEntity> implements PlanShareDao {
...
/**
* 根据分享用户 ID 列表查询分享记录
*
* @param shareUserIds 分享用户 ID 列表
* @return 分享记录列表
*/
@Override
public PageList<ShareEntity> listByShareUserIds(Integer pageSize, Integer pageNum, String orderKey, String orderType, String agencyId, List<String> shareUserIds) {
Page<ShareEntity> page = Page.of(pageNum, pageSize);
Page<ShareEntity> pageData = this.baseMapper.listByShareUserIds(page, agencyId, shareUserIds, orderKey, orderType);
return PageUtil.toPage(pageData);
}
...
}
PlanShareMapper.xml
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.learninggenie.common.data.mapper.mybatisplus.lesson2.PlanShareMapper">
<select id="listByShareUserIds" resultType="com.learninggenie.common.data.entity.lesson2.plan.ShareEntity">
SELECT s.*, share_up.DisplayName AS ShareUsername, create_up.DisplayName AS CreateUsername
FROM lessons2_plan_share s
JOIN authentication_user share_user ON s.ShareUserId = share_user.Id
JOIN authentication_user_profile share_up ON share_user.Id = share_up.UserId
JOIN authentication_user create_user ON s.CreateUserId = create_user.Id
JOIN authentication_user_profile create_up ON create_user.Id = create_up.UserId
WHERE s.IsDeleted = 0
AND s.AgencyId = #{agencyId}
<if test="shareUserIds != null and shareUserIds.size() > 0">
AND s.ShareUserId IN
<foreach collection="shareUserIds" item="shareUserId" open="(" close=")" separator=",">
#{shareUserId}
foreach>
if>
<choose>
<when test="orderKey == 'SHARE_USER'">
ORDER BY share_up.DisplayName ${orderType}, s.UpdateAtUtc DESC
when>
<when test="orderKey == 'CREATE_SHARE_USER'">
ORDER BY create_up.DisplayName ${orderType}, s.UpdateAtUtc DESC
when>
<otherwise>
ORDER BY s.UpdateAtUtc DESC
otherwise>
choose>
select>
...
Mapper>
PlanShareMapper.java
@Mapper
@Component
public interface PlanShareMapper extends BaseMapper<ShareEntity> {
...
/**
* 根据分享用户 ID 列表查询分享记录
* @param pageList 分页参数
* @param agencyId 机构 ID
* @param shareUserIds 分享用户 ID 列表
* @param orderKey 排序 Key
* @param orderType 排序类型
* @return 分享记录列表
*/
Page<ShareEntity> listByShareUserIds(@Param("pageList") Page<ShareEntity> pageList,
@Param("agencyId") String agencyId,
@Param("shareUserIds") List<String> shareUserIds,
@Param("orderKey") String orderKey,
@Param("orderType") String orderType);
...
}
ShareEntity.class
@TableName("lessons2_plan_share")
public class ShareEntity {
@ApiModelProperty(value = "ID", required = true)
@TableId(value = "Id")
private String id;
@ApiModelProperty(value = "分享的是哪个老师", required = true)
@TableField(value = "ShareUserId")
private String shareUserId;
@ApiModelProperty(value = "分享人姓名")
@TableField(exist = false)
private String shareUsername;
...
}
PageUtil.class
package com.learninggenie.common.data.utils;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.learninggenie.common.data.entity.PageList;
import java.util.function.Function;
import java.util.stream.Collectors;
public class PageUtil {
/**
* 将 Mybatis Plus 分页转换为 Dao层分页
*
* @param mpPage Mybatis Plus 分页
* @param 实体类型
* @return Dao层分页
*/
public static <T> PageList<T> toPage(IPage<T> mpPage) {
PageList<T> page = new PageList<>();
page.setRecords(mpPage.getRecords());
page.setPageNum(mpPage.getCurrent());
page.setPageSize(mpPage.getSize());
page.setTotal(mpPage.getTotal());
return page;
}
/**
* 分页类型转换
*
* @param pageList 源分页信息
* @param mapper 转换函数
* @param 源类型
* @param 目标类型
* @return 目标类型分页信息
*/
public static <T, R> PageList<R> map(PageList<T> pageList, Function<T, R> mapper) {
PageList<R> page = new PageList<>();
page.setRecords(pageList.getRecords().stream().map(mapper).collect(Collectors.toList()));
page.setPageNum(pageList.getPageNum());
page.setPageSize(pageList.getPageSize());
page.setTotal(pageList.getTotal());
return page;
}
/**
* @param pageNum 当前页
* @param pageSize 页大小
* @param 目标类型
* @return 空页面分页信息
*/
public static <T> PageList<T> emptyPage(Long pageNum, Long pageSize) {
PageList<T> page = new PageList<>();
page.setPageNum(pageNum);
page.setPageSize(pageSize);
page.setTotal(0L);
return page;
}
}
自定义类 Element 树形数据表格并实现表头排序
// 表格
<table class="table" style="font-family: SourceSansPro-Regular;font-weight: Regular;color: #222222;">
// 表头固定 存在滑动 border 消失问题
<thead style="position: sticky;top: 0px;">
<tr class="measure">
<td style="background-color: #f2f2f2;max-width: 300px;">
<span class="font-bold" style="padding-left: 25px;">{{$t('loc.mses')}}span>
td>
<td style="background-color: #e7f2ff;width: 240px;" class="td-center" @click="observationSort('noObservationCount')">
<span class="font-bold">{{$t('loc.noObservation')}}span>
<span class="pos-rlt">
<span style="left: 5px;" class="fa fa-caret-up pos-abt lg-sort-asc" :class="showAscSign('noObservationCount')">span>
<span style="left: 5px;" class="fa fa-caret-down pos-abt lg-sort-desc" :class="showDescSign('noObservationCount')">span>
span>
td>
<td style="background-color: #edfae7;min-width: 260px;" class="td-center" @click="observationSort('oneObservationCount')">
<span class="font-bold">{{$t('loc.observationCount1')}}span>
<span class="pos-rlt">
<span style="left: 5px;" class="fa fa-caret-up pos-abt lg-sort-asc" :class="showAscSign('oneObservationCount')">span>
<span style="left: 5px;" class="fa fa-caret-down pos-abt lg-sort-desc" :class="showDescSign('oneObservationCount')">span>
span>
td>
<td style="background-color: #ffffea;min-width: 240px;" class="td-center" @click="observationSort('multiObservationCount')">
<span class="font-bold">{{$t('loc.observationCountMore2')}}span>
<span class="pos-rlt">
<span style="left: 5px;" :class="showAscSign('multiObservationCount')" class="fa fa-caret-up pos-abt lg-sort-asc">span>
<span style="left: 5px;" :class="showDescSign('multiObservationCount')" class="fa fa-caret-down pos-abt lg-sort-desc">span>
span>
td>
tr>
thead>
<tbody v-for="(measure, index) in copyObservationData" :key="index">
<tr @click="openMeasureDetail(measure)" class="measure bg-light text-black-mon">
<td>
<div style="display: flex;align-items: center;">
<span v-if="measure.open" class="fa fa-caret-up m-r-xs">span>
<span v-if="!measure.open" class="fa fa-caret-down m-r-xs">span>
<div>
<span class="font-bold">{{measure.measureAbbr}}span>
<span v-if="measure.measureAbbr"><br/>span>
<span>{{measure.measureName}}span>
div>
div>
td>
<td class="td-center">
<span class="font-bold">{{measure.noObservationCount}}span>
td>
<td class="td-center">
<span class="font-bold">{{measure.oneObservationCount}}span>
td>
<td class="td-center">
<span class="font-bold">{{measure.multiObservationCount}}span>
td>
tr>
<tr v-if="measure.open" class="measure">
<td style="vertical-align: middle;">
<span style="padding-left: 15px">span>
td>
<td>
<span :class="colorShow(childrenInfo[child])" v-for="(child, index) in measure.noObservationChildren" :key="index">
{{childrenInfo[child].name}}<span v-if="index != measure.noObservationChildren.length-1">,span>
span>
td>
<td>
<span :class="colorShow(childrenInfo[child])" v-for="(child, index) in measure.oneObservationChildren" :key="index">
{{childrenInfo[child].name}}<span v-if="index != measure.oneObservationChildren.length-1">,span>
span>
td>
<td>
<span :class="colorShow(childrenInfo[child])" v-for="(child, index) in measure.multiObservationChildren" :key="index">
{{childrenInfo[child].name}}<span v-if="index != measure.multiObservationChildren.length-1">,span>
span>
td>
tr>
tbody>
table>
// 实现排序
data () {
...
sortStatus: {
currentSort: '', // 当前排序字段
noObservationCount: 0,
oneObservationCount: 0,
multiObservationCount: 0
}
...
}
methods:{
...
// 展开测评点下小孩信息
openMeasureDetail (measure) {
this.$set(measure, 'open', !measure.open);
}
// 测评点观察排序
observationSort (observationType) {
// 置空当前排序条件外排序状态
this.sortStatus.currentSort = observationType;
var currentSortStatus = this.sortStatus[observationType];
if (currentSortStatus == 2) {
currentSortStatus = 0;
this.sortStatus[observationType] = 0;
} else {
currentSortStatus += 1;
this.sortStatus[observationType] += 1;
}
this.copyObservationData = this.observationData.concat();
if (!currentSortStatus) {
return;
}
this.copyObservationData.sort(function(a,b) {
var x = a[observationType];
var y = b[observationType];
if (currentSortStatus == 1) {
// 升序
return ((x<y)?-1:(x>y)?1:0);
} else {
// 降序
return ((x<y)?1:(x>y)?-1:0);
}
});
}
...
}