✨(table): add sub-table tree-table
This commit is contained in:
parent
1f5313fd94
commit
3e68cdf169
183
package/component/src/component/table/TableRow.vue
Normal file
183
package/component/src/component/table/TableRow.vue
Normal 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>
|
@ -12,10 +12,6 @@
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.layui-table tr {
|
||||
// display: flex;
|
||||
}
|
||||
|
||||
.layui-table th {
|
||||
text-align: left;
|
||||
font-weight: 400;
|
||||
@ -112,7 +108,7 @@
|
||||
.layui-table-view .layui-table {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
border-collapse: separate;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.layui-table-view .layui-table[lay-skin="line"] {
|
||||
@ -240,8 +236,8 @@
|
||||
.layui-table-tool-panel li {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
// white-space: nowrap;
|
||||
}
|
||||
|
||||
.layui-table-call-ellipsis{
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
@ -360,6 +356,12 @@
|
||||
-webkit-box-pack: center;
|
||||
}
|
||||
|
||||
.layui-table-cell-expand-icon {
|
||||
border: 1px solid #eee;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.layui-table-body {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
@ -370,7 +372,7 @@
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
background-clip: padding-box;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 7px;
|
||||
@ -635,3 +637,7 @@ body .layui-table-tips .layui-layer-content {
|
||||
.layui-table-view {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.layui-table-cell-expand{
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
@ -6,7 +6,7 @@ export default {
|
||||
|
||||
<script setup lang="ts">
|
||||
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 { Recordable } from "../../types";
|
||||
import LayCheckbox from "../checkbox/index.vue";
|
||||
@ -14,6 +14,7 @@ import LayDropdown from "../dropdown/index.vue";
|
||||
import LayTooltip from "../tooltip/index.vue";
|
||||
import { LayIcon } from "@layui/icons-vue";
|
||||
import LayPage from "../page/index.vue";
|
||||
import TableRow from "./TableRow.vue";
|
||||
|
||||
export interface LayTableProps {
|
||||
id?: string;
|
||||
@ -49,7 +50,7 @@ const slots = slot.default && slot.default();
|
||||
|
||||
const allChecked = ref(false);
|
||||
const tableDataSource = ref([...props.dataSource]);
|
||||
const tableSelectedKeys = ref([...props.selectedKeys]);
|
||||
const tableSelectedKeys = ref<Recordable[]>([...props.selectedKeys]);
|
||||
const tableColumns = ref([...props.columns]);
|
||||
const tableColumnKeys = ref(
|
||||
props.columns.map((item: any) => {
|
||||
@ -81,7 +82,7 @@ const changeAll = function (checked: any) {
|
||||
|
||||
watch(
|
||||
tableSelectedKeys,
|
||||
function () {
|
||||
() => {
|
||||
if (tableSelectedKeys.value.length === props.dataSource.length) {
|
||||
allChecked.value = true;
|
||||
} else {
|
||||
@ -108,7 +109,6 @@ const contextmenu = function (data: any, evt: MouseEvent) {
|
||||
emit("contextmenu", data, evt);
|
||||
};
|
||||
|
||||
// 打印 table 数据
|
||||
const print = function () {
|
||||
let subOutputRankPrint = document.getElementById(tableId) as HTMLElement;
|
||||
let newContent = subOutputRankPrint.innerHTML;
|
||||
@ -163,16 +163,12 @@ function exportToExcel(headerList: any, bodyList: any) {
|
||||
}
|
||||
|
||||
const sortTable = (e: any, key: string, sort: string) => {
|
||||
// 当前排序
|
||||
let currentSort = e.target.parentNode.getAttribute("lay-sort");
|
||||
// 点击排序
|
||||
if (sort === "desc") {
|
||||
if (currentSort === sort) {
|
||||
// 取消排序
|
||||
e.target.parentNode.setAttribute("lay-sort", "");
|
||||
tableDataSource.value = [...props.dataSource];
|
||||
} else {
|
||||
// 进行 desc 排序
|
||||
e.target.parentNode.setAttribute("lay-sort", "desc");
|
||||
tableDataSource.value.sort((x, y) => {
|
||||
if (x[key] < y[key]) return 1;
|
||||
@ -182,11 +178,9 @@ const sortTable = (e: any, key: string, sort: string) => {
|
||||
}
|
||||
} else {
|
||||
if (currentSort === sort) {
|
||||
// 取消排序
|
||||
e.target.parentNode.setAttribute("lay-sort", "");
|
||||
tableDataSource.value = [...props.dataSource];
|
||||
} else {
|
||||
// 进行 asc 排序
|
||||
e.target.parentNode.setAttribute("lay-sort", "asc");
|
||||
tableDataSource.value.sort((x, y) => {
|
||||
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 tableBody = ref<HTMLElement | null>(null);
|
||||
|
||||
// 拖动监听
|
||||
onMounted(() => {
|
||||
tableBody.value?.addEventListener("scroll", () => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
@ -315,86 +316,26 @@ onMounted(() => {
|
||||
<div class="layui-table-body layui-table-main" ref="tableBody">
|
||||
<table class="layui-table" :lay-size="size">
|
||||
<tbody>
|
||||
<!-- 渲染 -->
|
||||
<template v-for="data in tableDataSource" :key="data">
|
||||
<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"
|
||||
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"
|
||||
<table-row
|
||||
:id="id"
|
||||
: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',
|
||||
}"
|
||||
:columns="columns"
|
||||
:checkbox="checkbox"
|
||||
:tableColumnKeys="tableColumnKeys"
|
||||
@row="rowClick"
|
||||
@row-double="rowDoubleClick"
|
||||
@contextmenu="contextmenu"
|
||||
v-model:selectedKeys="tableSelectedKeys"
|
||||
>
|
||||
<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 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>
|
||||
</template>
|
||||
</template>
|
||||
</tr>
|
||||
</table-row>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -410,8 +351,12 @@ onMounted(() => {
|
||||
show-skip
|
||||
@jump="change"
|
||||
>
|
||||
<template #prev><lay-icon type="layui-icon-left" /></template>
|
||||
<template #next><lay-icon type="layui-icon-right" /></template>
|
||||
<template #prev>
|
||||
<lay-icon type="layui-icon-left" />
|
||||
</template>
|
||||
<template #next>
|
||||
<lay-icon type="layui-icon-right" />
|
||||
</template>
|
||||
</lay-page>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,7 +5,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { StringOrNumber } from "./tree.type";
|
||||
import { StringOrNumber, CustomKey, CustomString } from "./tree.type";
|
||||
import { LayIcon } from "@layui/icons-vue";
|
||||
import LayCheckbox from "../checkbox/index.vue";
|
||||
import { Ref, useSlots } from "vue";
|
||||
@ -13,10 +13,7 @@ import { Tree } from "./tree";
|
||||
import { Nullable } from "../../types";
|
||||
import LayTransition from "../transition/index.vue";
|
||||
|
||||
type CustomKey = string | number;
|
||||
type CustomString = (() => string) | string;
|
||||
|
||||
interface TreeData {
|
||||
export interface TreeData {
|
||||
id: CustomKey;
|
||||
title: CustomString;
|
||||
children: TreeData[];
|
||||
@ -29,7 +26,7 @@ interface TreeData {
|
||||
parentNode: Nullable<TreeData>;
|
||||
}
|
||||
|
||||
interface TreeNodeProps {
|
||||
export interface TreeNodeProps {
|
||||
tree: Tree;
|
||||
nodeList: TreeData[];
|
||||
showCheckbox: boolean;
|
||||
@ -113,9 +110,9 @@ function handleTitleClick(node: TreeData) {
|
||||
{ 'layui-tree-iconClick': true },
|
||||
]"
|
||||
>
|
||||
<LayIcon :type="nodeIconType(node)" @click="handleIconClick(node)" />
|
||||
<lay-icon :type="nodeIconType(node)" @click="handleIconClick(node)" />
|
||||
</span>
|
||||
<LayCheckbox
|
||||
<lay-checkbox
|
||||
v-if="showCheckbox"
|
||||
:modelValue="node.isChecked.value"
|
||||
:disabled="node.isDisabled.value"
|
||||
@ -138,13 +135,13 @@ function handleTitleClick(node: TreeData) {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<LayTransition :enable="collapseTransition">
|
||||
<lay-transition :enable="collapseTransition">
|
||||
<div
|
||||
v-if="node.isLeaf.value"
|
||||
class="layui-tree-pack layui-tree-showLine"
|
||||
style="display: block"
|
||||
>
|
||||
<TreeNode
|
||||
<tree-node
|
||||
:node-list="node.children"
|
||||
:show-checkbox="showCheckbox"
|
||||
:show-line="showLine"
|
||||
@ -154,8 +151,6 @@ function handleTitleClick(node: TreeData) {
|
||||
@node-click="recursiveNodeClick"
|
||||
/>
|
||||
</div>
|
||||
</LayTransition>
|
||||
</lay-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -32,3 +32,6 @@ export interface TreeEmits {
|
||||
(e: "update:expandKeys", keys: KeysType): void;
|
||||
(e: "node-click", node: OriginalTreeData, event: Event): void;
|
||||
}
|
||||
|
||||
export type CustomKey = string | number;
|
||||
export type CustomString = (() => string) | string;
|
||||
|
@ -237,6 +237,11 @@ export default {
|
||||
<lay-button size="xs">修改</lay-button>
|
||||
<lay-button size="xs" type="primary">删除</lay-button>
|
||||
</template>
|
||||
<template v-slot:expand="{ data }">
|
||||
<div style="height:100px;">
|
||||
内容
|
||||
</div>
|
||||
</template>
|
||||
</lay-table>
|
||||
</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 属性
|
||||
:::
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user