(table): add sub-table tree-table

This commit is contained in:
就眠儀式 2022-05-28 23:50:41 +08:00
parent 1f5313fd94
commit 3e68cdf169
6 changed files with 345 additions and 111 deletions

View File

@ -0,0 +1,183 @@
<script lang="ts">
export default {
name: "TableRow",
};
</script>
<script lang="ts" setup>
import { computed, ref, useSlots, WritableComputedRef } from "vue";
import { Recordable } from "../../types";
export interface LayTableProps {
selectedKeys: Recordable[];
tableColumnKeys: Recordable[];
columns: Recordable[];
checkbox?: boolean;
id: string;
data: any;
}
const slot = useSlots();
const emit = defineEmits([
"row",
"row-double",
"contextmenu",
"update:selectedKeys",
]);
const props = withDefaults(defineProps<LayTableProps>(), {
checkbox: false,
});
const tableSelectedKeys: WritableComputedRef<Recordable[]> = computed({
get() {
return [...props.selectedKeys];
},
set(val) {
emit("update:selectedKeys", val);
},
});
const isExpand = ref(false);
const slotsData = ref<string[]>([]);
props.columns.map((value: any) => {
if (value.customSlot) {
slotsData.value.push(value.customSlot);
}
});
const rowClick = function (data: any, evt: MouseEvent) {
emit("row", data, evt);
};
const rowDoubleClick = function (data: any, evt: MouseEvent) {
emit("row-double", data, evt);
};
const contextmenu = function (data: any, evt: MouseEvent) {
emit("contextmenu", data, evt);
};
const expandIconType = computed(() => {
return isExpand.value ? "layui-icon-subtraction" : "layui-icon-addition";
});
const handleExpand = () => {
isExpand.value = !isExpand.value;
}
</script>
<template>
<tr
@click.stop="rowClick(data, $event)"
@dblclick.stop="rowDoubleClick(data, $event)"
@contextmenu.stop="contextmenu(data, $event)"
>
<!-- 复选框 -->
<td v-if="checkbox" class="layui-table-col-special">
<div class="layui-table-cell laytable-cell-checkbox">
<lay-checkbox
v-model="tableSelectedKeys"
:label="data[id]"
skin="primary"
/>
</div>
</td>
<!-- 数据列 -->
<template v-for="(column, index) in columns" :key="column">
<!-- 展示否 -->
<template v-if="tableColumnKeys.includes(column.key)">
<!-- 插槽列 -->
<template v-if="column.customSlot">
<td
class="layui-table-cell"
:style="{
textAlign: column.align,
width: column.width ? column.width : '0',
minWidth: column.minWidth ? column.minWidth : '47px',
whiteSpace: column.ellipsisTooltip ? 'nowrap' : 'normal',
}"
>
<lay-icon
v-if="(slot.expand || data.children) && index === 0"
class="layui-table-cell-expand-icon"
:type="expandIconType"
@click="handleExpand"
></lay-icon>
<lay-tooltip
v-if="column.ellipsisTooltip"
:content="data[column.key]"
:isAutoShow="true"
>
<slot :name="column.customSlot" :data="data"></slot>
</lay-tooltip>
<slot v-else :name="column.customSlot" :data="data"></slot>
</td>
</template>
<!-- Column -->
<template v-else>
<template v-if="column.key in data">
<td
class="layui-table-cell"
:style="{
textAlign: column.align,
width: column.width ? column.width : '0',
minWidth: column.minWidth ? column.minWidth : '47px',
whiteSpace: column.ellipsisTooltip ? 'nowrap' : 'normal',
}"
>
<lay-icon
v-if="(slot.expand || data.children) && index === 0"
class="layui-table-cell-expand-icon"
:type="expandIconType"
@click="handleExpand"
></lay-icon>
<lay-tooltip
v-if="column.ellipsisTooltip"
:content="data[column.key]"
:isAutoShow="true"
>
{{ data[column.key] }}
</lay-tooltip>
<span v-else> {{ data[column.key] }} </span>
</td>
</template>
</template>
</template>
</template>
</tr>
<!-- 嵌套表单 -->
<tr class="layui-table-cell-expand" v-if="slot.expand && isExpand">
<slot name="expand"></slot>
</tr>
<!-- 树形结构 -->
<template v-if="data.children && isExpand">
<template v-for="(children, index) in data.children" :key="index">
<table-row
:id="id"
:data="children"
:columns="columns"
:checkbox="checkbox"
:tableColumnKeys="tableColumnKeys"
@row="rowClick"
@row-double="rowDoubleClick"
@contextmenu="contextmenu"
v-model:selectedKeys="tableSelectedKeys"
>
<template v-for="name in slotsData" #[name]>
<slot :name="name" :data="data"></slot>
</template>
<template v-if="slot.expand" #expand>
<slot name="expand" :data="data"></slot>
</template>
</table-row>
</template>
</template>
</template>

View File

@ -12,10 +12,6 @@
table-layout: fixed; table-layout: fixed;
} }
.layui-table tr {
// display: flex;
}
.layui-table th { .layui-table th {
text-align: left; text-align: left;
font-weight: 400; font-weight: 400;
@ -112,7 +108,7 @@
.layui-table-view .layui-table { .layui-table-view .layui-table {
position: relative; position: relative;
margin: 0; margin: 0;
border-collapse: separate; border-collapse: collapse;
} }
.layui-table-view .layui-table[lay-skin="line"] { .layui-table-view .layui-table[lay-skin="line"] {
@ -240,8 +236,8 @@
.layui-table-tool-panel li { .layui-table-tool-panel li {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
// white-space: nowrap;
} }
.layui-table-call-ellipsis{ .layui-table-call-ellipsis{
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
@ -360,6 +356,12 @@
-webkit-box-pack: center; -webkit-box-pack: center;
} }
.layui-table-cell-expand-icon {
border: 1px solid #eee;
margin-right: 8px;
border-radius: 2px;
}
.layui-table-body { .layui-table-body {
position: relative; position: relative;
overflow: auto; overflow: auto;
@ -370,7 +372,7 @@
} }
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.4); background-color: rgba(0, 0, 0, 0.2);
background-clip: padding-box; background-clip: padding-box;
border: 3px solid transparent; border: 3px solid transparent;
border-radius: 7px; border-radius: 7px;
@ -635,3 +637,7 @@ body .layui-table-tips .layui-layer-content {
.layui-table-view { .layui-table-view {
margin: 10px 0; margin: 10px 0;
} }
.layui-table-cell-expand{
border-bottom: 1px solid #eee;
}

View File

@ -6,7 +6,7 @@ export default {
<script setup lang="ts"> <script setup lang="ts">
import "./index.less"; import "./index.less";
import { ref, watch, useSlots, withDefaults, onMounted } from "vue"; import { ref, watch, useSlots, withDefaults, onMounted, Ref } from "vue";
import { v4 as uuidv4 } from "../../utils/guidUtil"; import { v4 as uuidv4 } from "../../utils/guidUtil";
import { Recordable } from "../../types"; import { Recordable } from "../../types";
import LayCheckbox from "../checkbox/index.vue"; import LayCheckbox from "../checkbox/index.vue";
@ -14,6 +14,7 @@ import LayDropdown from "../dropdown/index.vue";
import LayTooltip from "../tooltip/index.vue"; import LayTooltip from "../tooltip/index.vue";
import { LayIcon } from "@layui/icons-vue"; import { LayIcon } from "@layui/icons-vue";
import LayPage from "../page/index.vue"; import LayPage from "../page/index.vue";
import TableRow from "./TableRow.vue";
export interface LayTableProps { export interface LayTableProps {
id?: string; id?: string;
@ -49,7 +50,7 @@ const slots = slot.default && slot.default();
const allChecked = ref(false); const allChecked = ref(false);
const tableDataSource = ref([...props.dataSource]); const tableDataSource = ref([...props.dataSource]);
const tableSelectedKeys = ref([...props.selectedKeys]); const tableSelectedKeys = ref<Recordable[]>([...props.selectedKeys]);
const tableColumns = ref([...props.columns]); const tableColumns = ref([...props.columns]);
const tableColumnKeys = ref( const tableColumnKeys = ref(
props.columns.map((item: any) => { props.columns.map((item: any) => {
@ -81,7 +82,7 @@ const changeAll = function (checked: any) {
watch( watch(
tableSelectedKeys, tableSelectedKeys,
function () { () => {
if (tableSelectedKeys.value.length === props.dataSource.length) { if (tableSelectedKeys.value.length === props.dataSource.length) {
allChecked.value = true; allChecked.value = true;
} else { } else {
@ -108,7 +109,6 @@ const contextmenu = function (data: any, evt: MouseEvent) {
emit("contextmenu", data, evt); emit("contextmenu", data, evt);
}; };
// table
const print = function () { const print = function () {
let subOutputRankPrint = document.getElementById(tableId) as HTMLElement; let subOutputRankPrint = document.getElementById(tableId) as HTMLElement;
let newContent = subOutputRankPrint.innerHTML; let newContent = subOutputRankPrint.innerHTML;
@ -163,16 +163,12 @@ function exportToExcel(headerList: any, bodyList: any) {
} }
const sortTable = (e: any, key: string, sort: string) => { const sortTable = (e: any, key: string, sort: string) => {
//
let currentSort = e.target.parentNode.getAttribute("lay-sort"); let currentSort = e.target.parentNode.getAttribute("lay-sort");
//
if (sort === "desc") { if (sort === "desc") {
if (currentSort === sort) { if (currentSort === sort) {
//
e.target.parentNode.setAttribute("lay-sort", ""); e.target.parentNode.setAttribute("lay-sort", "");
tableDataSource.value = [...props.dataSource]; tableDataSource.value = [...props.dataSource];
} else { } else {
// desc
e.target.parentNode.setAttribute("lay-sort", "desc"); e.target.parentNode.setAttribute("lay-sort", "desc");
tableDataSource.value.sort((x, y) => { tableDataSource.value.sort((x, y) => {
if (x[key] < y[key]) return 1; if (x[key] < y[key]) return 1;
@ -182,11 +178,9 @@ const sortTable = (e: any, key: string, sort: string) => {
} }
} else { } else {
if (currentSort === sort) { if (currentSort === sort) {
//
e.target.parentNode.setAttribute("lay-sort", ""); e.target.parentNode.setAttribute("lay-sort", "");
tableDataSource.value = [...props.dataSource]; tableDataSource.value = [...props.dataSource];
} else { } else {
// asc
e.target.parentNode.setAttribute("lay-sort", "asc"); e.target.parentNode.setAttribute("lay-sort", "asc");
tableDataSource.value.sort((x, y) => { tableDataSource.value.sort((x, y) => {
if (x[key] < y[key]) return -1; if (x[key] < y[key]) return -1;
@ -200,12 +194,19 @@ const sortTable = (e: any, key: string, sort: string) => {
let tableHeader = ref<HTMLElement | null>(null); let tableHeader = ref<HTMLElement | null>(null);
let tableBody = ref<HTMLElement | null>(null); let tableBody = ref<HTMLElement | null>(null);
//
onMounted(() => { onMounted(() => {
tableBody.value?.addEventListener("scroll", () => { tableBody.value?.addEventListener("scroll", () => {
tableHeader.value!.scrollLeft = tableBody.value?.scrollLeft || 0; tableHeader.value!.scrollLeft = tableBody.value?.scrollLeft || 0;
}); });
}); });
const slotsData = ref<string[]>([]);
props.columns.map((value: any) => {
if (value.customSlot) {
slotsData.value.push(value.customSlot);
}
});
</script> </script>
<template> <template>
@ -315,86 +316,26 @@ onMounted(() => {
<div class="layui-table-body layui-table-main" ref="tableBody"> <div class="layui-table-body layui-table-main" ref="tableBody">
<table class="layui-table" :lay-size="size"> <table class="layui-table" :lay-size="size">
<tbody> <tbody>
<!-- 渲染 -->
<template v-for="data in tableDataSource" :key="data"> <template v-for="data in tableDataSource" :key="data">
<tr <table-row
@click.stop="rowClick(data, $event)" :id="id"
@dblclick.stop="rowDoubleClick(data, $event)"
@contextmenu.stop="contextmenu(data, $event)"
>
<!-- 复选框 -->
<td v-if="checkbox" class="layui-table-col-special">
<div class="layui-table-cell laytable-cell-checkbox">
<lay-checkbox
v-model="tableSelectedKeys"
skin="primary"
:label="data[id]"
/>
</div>
</td>
<!-- 数据列 -->
<template v-for="column in columns" :key="column">
<!-- 展示否 -->
<template v-if="tableColumnKeys.includes(column.key)">
<!-- 插槽列 -->
<template v-if="column.customSlot">
<td
class="layui-table-cell"
:style="{
textAlign: column.align,
width: column.width ? column.width : '0',
minWidth: column.minWidth
? column.minWidth
: '47px',
whiteSpace: column.ellipsisTooltip
? 'nowrap'
: 'normal',
}"
>
<lay-tooltip
v-if="column.ellipsisTooltip"
:content="data[column.key]"
:isAutoShow="true"
>
<slot :name="column.customSlot" :data="data"></slot>
</lay-tooltip>
<slot
v-else
:name="column.customSlot"
:data="data" :data="data"
></slot> :columns="columns"
</td> :checkbox="checkbox"
</template> :tableColumnKeys="tableColumnKeys"
<!-- Column --> @row="rowClick"
<template v-else> @row-double="rowDoubleClick"
<template v-if="column.key in data"> @contextmenu="contextmenu"
<td v-model:selectedKeys="tableSelectedKeys"
class="layui-table-cell"
:style="{
textAlign: column.align,
width: column.width ? column.width : '0',
minWidth: column.minWidth
? column.minWidth
: '47px',
whiteSpace: column.ellipsisTooltip
? 'nowrap'
: 'normal',
}"
> >
<lay-tooltip <template v-for="name in slotsData" #[name]>
v-if="column.ellipsisTooltip" <slot :name="name" :data="data"></slot>
:content="data[column.key]"
:isAutoShow="true"
>
{{ data[column.key] }}
</lay-tooltip>
<span v-else> {{ data[column.key] }} </span>
</td>
</template> </template>
<template v-if="slot.expand" #expand>
<slot name="expand" :data="data"></slot>
</template> </template>
</template> </table-row>
</template>
</tr>
</template> </template>
</tbody> </tbody>
</table> </table>
@ -410,8 +351,12 @@ onMounted(() => {
show-skip show-skip
@jump="change" @jump="change"
> >
<template #prev><lay-icon type="layui-icon-left" /></template> <template #prev>
<template #next><lay-icon type="layui-icon-right" /></template> <lay-icon type="layui-icon-left" />
</template>
<template #next>
<lay-icon type="layui-icon-right" />
</template>
</lay-page> </lay-page>
</div> </div>
</div> </div>

View File

@ -5,7 +5,7 @@ export default {
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import { StringOrNumber } from "./tree.type"; import { StringOrNumber, CustomKey, CustomString } from "./tree.type";
import { LayIcon } from "@layui/icons-vue"; import { LayIcon } from "@layui/icons-vue";
import LayCheckbox from "../checkbox/index.vue"; import LayCheckbox from "../checkbox/index.vue";
import { Ref, useSlots } from "vue"; import { Ref, useSlots } from "vue";
@ -13,10 +13,7 @@ import { Tree } from "./tree";
import { Nullable } from "../../types"; import { Nullable } from "../../types";
import LayTransition from "../transition/index.vue"; import LayTransition from "../transition/index.vue";
type CustomKey = string | number; export interface TreeData {
type CustomString = (() => string) | string;
interface TreeData {
id: CustomKey; id: CustomKey;
title: CustomString; title: CustomString;
children: TreeData[]; children: TreeData[];
@ -29,7 +26,7 @@ interface TreeData {
parentNode: Nullable<TreeData>; parentNode: Nullable<TreeData>;
} }
interface TreeNodeProps { export interface TreeNodeProps {
tree: Tree; tree: Tree;
nodeList: TreeData[]; nodeList: TreeData[];
showCheckbox: boolean; showCheckbox: boolean;
@ -113,9 +110,9 @@ function handleTitleClick(node: TreeData) {
{ 'layui-tree-iconClick': true }, { 'layui-tree-iconClick': true },
]" ]"
> >
<LayIcon :type="nodeIconType(node)" @click="handleIconClick(node)" /> <lay-icon :type="nodeIconType(node)" @click="handleIconClick(node)" />
</span> </span>
<LayCheckbox <lay-checkbox
v-if="showCheckbox" v-if="showCheckbox"
:modelValue="node.isChecked.value" :modelValue="node.isChecked.value"
:disabled="node.isDisabled.value" :disabled="node.isDisabled.value"
@ -138,13 +135,13 @@ function handleTitleClick(node: TreeData) {
</span> </span>
</div> </div>
</div> </div>
<LayTransition :enable="collapseTransition"> <lay-transition :enable="collapseTransition">
<div <div
v-if="node.isLeaf.value" v-if="node.isLeaf.value"
class="layui-tree-pack layui-tree-showLine" class="layui-tree-pack layui-tree-showLine"
style="display: block" style="display: block"
> >
<TreeNode <tree-node
:node-list="node.children" :node-list="node.children"
:show-checkbox="showCheckbox" :show-checkbox="showCheckbox"
:show-line="showLine" :show-line="showLine"
@ -154,8 +151,6 @@ function handleTitleClick(node: TreeData) {
@node-click="recursiveNodeClick" @node-click="recursiveNodeClick"
/> />
</div> </div>
</LayTransition> </lay-transition>
</div> </div>
</template> </template>
<style scoped></style>

View File

@ -32,3 +32,6 @@ export interface TreeEmits {
(e: "update:expandKeys", keys: KeysType): void; (e: "update:expandKeys", keys: KeysType): void;
(e: "node-click", node: OriginalTreeData, event: Event): void; (e: "node-click", node: OriginalTreeData, event: Event): void;
} }
export type CustomKey = string | number;
export type CustomString = (() => string) | string;

View File

@ -237,6 +237,11 @@ export default {
<lay-button size="xs">修改</lay-button> <lay-button size="xs">修改</lay-button>
<lay-button size="xs" type="primary">删除</lay-button> <lay-button size="xs" type="primary">删除</lay-button>
</template> </template>
<template v-slot:expand="{ data }">
<div style="height:100px;">
内容
</div>
</template>
</lay-table> </lay-table>
</template> </template>
@ -318,6 +323,103 @@ export default {
::: :::
::: title 开启子表
:::
::: demo 当表格内容较多不能一次性完全展示时。
<template>
<lay-table :columns="columns6" :dataSource="dataSource6">
<template v-slot:expand="{ data }">
{{ data }}
</template>
</lay-table>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const columns6 = [
{
title:"姓名",
width:"200px",
key:"name"
},{
title:"成绩",
width: "180px",
key:"score"
}
]
const dataSource6 = [
{name:"张三", score:100},
{name:"李四", score:80},
{name:"王二", score:99},
{name:"麻子", score:92},
{name:"无名", score:60},
{name:"有名", score:70},
]
return {
columns6,
dataSource6
}
}
}
</script>
:::
::: title 树形表格
:::
::: demo 树形数据的展示,当数据中有 children 字段时会自动展示为树形表格
<template>
<lay-table :columns="columns7" :dataSource="dataSource7">
</lay-table>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const columns7 = [
{
title:"姓名",
width:"200px",
key:"name"
},{
title:"成绩",
width: "180px",
key:"score"
},
]
const dataSource7 = [
{name:"张三", score:100, children: [{name:"张三", score:100},{name:"张三", score:100}]},
{name:"李四", score:80, children: [{name:"张三", score:100},{name:"张三", score:100}]},
{name:"王二", score:99, children: [{name:"张三", score:100},{name:"张三", score:100}]},
{name:"麻子", score:92, children: [{name:"张三", score:100},{name:"张三", score:100}]},
{name:"无名", score:60, children: [{name:"张三", score:100},{name:"张三", score:100}]},
{name:"有名", score:70, children: [{name:"张三", score:100},{name:"张三", score:100}]},
]
return {
columns7,
dataSource7
}
}
}
</script>
:::
::: title Table 属性 ::: title Table 属性
::: :::