|
@@ -1,293 +0,0 @@
|
|
|
-<template>
|
|
|
|
|
- <view class="ie-tree-node">
|
|
|
|
|
- <!-- 节点内容 -->
|
|
|
|
|
- <view @click.stop="handleClick">
|
|
|
|
|
- <slot :node="nodeData" :parent="parentData">
|
|
|
|
|
- <!-- 默认节点内容 -->
|
|
|
|
|
- <view class="ie-tree-node__default">
|
|
|
|
|
- <uv-icon
|
|
|
|
|
- v-if="!isLeafNode"
|
|
|
|
|
- name="arrow-right"
|
|
|
|
|
- size="14"
|
|
|
|
|
- color="#888"
|
|
|
|
|
- :custom-class="['ie-tree-node__expand-icon', isExpanded ? 'ie-tree-node__expand-icon--expanded' : '']"
|
|
|
|
|
- />
|
|
|
|
|
- <text class="ie-tree-node__label">{{ nodeLabel }}</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- </slot>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 子节点容器 -->
|
|
|
|
|
- <view
|
|
|
|
|
- v-if="hasChildren"
|
|
|
|
|
- ref="childrenRef"
|
|
|
|
|
- class="ie-tree-node__children"
|
|
|
|
|
- :id="`ie-tree-children-${nodeKey}`"
|
|
|
|
|
- :class="['ie-tree-node__children', { 'ie-tree-node__children--measuring': isMeasuringHeight }]"
|
|
|
|
|
- :style="childrenStyle"
|
|
|
|
|
- >
|
|
|
|
|
- <ie-tree-node
|
|
|
|
|
- v-for="child in childrenData"
|
|
|
|
|
- :key="getNodeKey(child)"
|
|
|
|
|
- :node-data="child"
|
|
|
|
|
- :parent-data="nodeData"
|
|
|
|
|
- :tree-props="treeProps"
|
|
|
|
|
- @node-click="handleChildNodeClick"
|
|
|
|
|
- >
|
|
|
|
|
- <template #default="slotProps: { node: any; parent: any }">
|
|
|
|
|
- <slot :node="slotProps.node" :parent="slotProps.parent"></slot>
|
|
|
|
|
- </template>
|
|
|
|
|
- </ie-tree-node>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
-</template>
|
|
|
|
|
-
|
|
|
|
|
-<script lang="ts" setup>
|
|
|
|
|
-import { getCurrentInstance, computed, ref, nextTick } from 'vue';
|
|
|
|
|
-import IeTreeNode from './ie-tree-node.vue';
|
|
|
|
|
-import type { TreeProps } from '@/types';
|
|
|
|
|
-
|
|
|
|
|
-defineOptions({
|
|
|
|
|
- name: 'ie-tree-node',
|
|
|
|
|
- options: {
|
|
|
|
|
- virtualHost: true
|
|
|
|
|
- }
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const instance = getCurrentInstance();
|
|
|
|
|
-
|
|
|
|
|
-// Props
|
|
|
|
|
-const props = defineProps<{
|
|
|
|
|
- nodeData: any; // 节点数据
|
|
|
|
|
- parentData?: any; // 父节点数据
|
|
|
|
|
- treeProps: TreeProps; // 树配置项
|
|
|
|
|
-}>();
|
|
|
|
|
-
|
|
|
|
|
-// Emits
|
|
|
|
|
-const emit = defineEmits<{
|
|
|
|
|
- (e: 'node-click', data: { node: any; parent: any | null }): void;
|
|
|
|
|
-}>();
|
|
|
|
|
-
|
|
|
|
|
-// Slots
|
|
|
|
|
-defineSlots<{
|
|
|
|
|
- default(props: { node: any; parent: any }): any;
|
|
|
|
|
-}>();
|
|
|
|
|
-
|
|
|
|
|
-// 默认配置
|
|
|
|
|
-const defaultTreeProps: Required<TreeProps> = {
|
|
|
|
|
- children: 'children',
|
|
|
|
|
- label: 'name',
|
|
|
|
|
- nodeKey: 'id',
|
|
|
|
|
- isLeaf: 'isLeaf',
|
|
|
|
|
- disabled: 'disabled',
|
|
|
|
|
- isExpanded: 'isExpanded'
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 合并配置
|
|
|
|
|
-const mergedProps = computed(() => ({
|
|
|
|
|
- ...defaultTreeProps,
|
|
|
|
|
- ...props.treeProps
|
|
|
|
|
-}));
|
|
|
|
|
-
|
|
|
|
|
-// 获取节点属性值
|
|
|
|
|
-const getNodeProp = (node: any, prop: string, defaultValue?: any) => {
|
|
|
|
|
- return node?.[prop] ?? defaultValue;
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 获取节点唯一标识
|
|
|
|
|
-const getNodeKey = (node: any): string | number => {
|
|
|
|
|
- const key = mergedProps.value.nodeKey;
|
|
|
|
|
- return getNodeProp(node, key, node?.value ?? node?.id ?? '');
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 获取节点标签
|
|
|
|
|
-const nodeLabel = computed(() => {
|
|
|
|
|
- return getNodeProp(props.nodeData, mergedProps.value.label, '');
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-// 获取子节点数据
|
|
|
|
|
-const childrenData = computed(() => {
|
|
|
|
|
- const childrenProp = mergedProps.value.children;
|
|
|
|
|
- return getNodeProp(props.nodeData, childrenProp, []) || [];
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-// 是否有子节点
|
|
|
|
|
-const hasChildren = computed(() => {
|
|
|
|
|
- return childrenData.value && childrenData.value.length > 0;
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-// 判断是否为叶子节点
|
|
|
|
|
-const isLeafNode = computed(() => {
|
|
|
|
|
- const isLeafProp = mergedProps.value.isLeaf;
|
|
|
|
|
- const isLeaf = getNodeProp(props.nodeData, isLeafProp);
|
|
|
|
|
- if (isLeaf !== undefined) {
|
|
|
|
|
- return isLeaf;
|
|
|
|
|
- }
|
|
|
|
|
- // 如果没有 isLeaf 属性,根据是否有子节点判断
|
|
|
|
|
- return !hasChildren.value;
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-// 是否展开
|
|
|
|
|
-const isExpanded = computed(() => {
|
|
|
|
|
- const expandedProp = mergedProps.value.isExpanded;
|
|
|
|
|
- return getNodeProp(props.nodeData, expandedProp, false);
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-// 节点唯一标识
|
|
|
|
|
-const nodeKey = computed(() => getNodeKey(props.nodeData));
|
|
|
|
|
-
|
|
|
|
|
-// 高度测量相关
|
|
|
|
|
-const isMeasuringHeight = ref(false);
|
|
|
|
|
-const childrenRef = ref();
|
|
|
|
|
-const measuredHeight = ref<number | string>(0);
|
|
|
|
|
-const isAnimating = ref(false);
|
|
|
|
|
-
|
|
|
|
|
-// 子节点容器样式
|
|
|
|
|
-const childrenStyle = computed(() => {
|
|
|
|
|
- if (isMeasuringHeight.value) {
|
|
|
|
|
- return { height: 'auto' };
|
|
|
|
|
- }
|
|
|
|
|
- const heightValue = typeof measuredHeight.value === 'number' ? `${measuredHeight.value}px` : measuredHeight.value;
|
|
|
|
|
- return { height: heightValue };
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-// 获取元素尺寸
|
|
|
|
|
-const getRect = (selector: string) => {
|
|
|
|
|
- return new Promise<{ top: number; height: number }>((resolve) => {
|
|
|
|
|
- const query = uni.createSelectorQuery().in(instance?.proxy);
|
|
|
|
|
- query.select(selector).boundingClientRect((rect) => {
|
|
|
|
|
- resolve(rect as { top: number; height: number });
|
|
|
|
|
- }).exec();
|
|
|
|
|
- });
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 测量子节点高度
|
|
|
|
|
-const measureHeight = () => {
|
|
|
|
|
- if (!hasChildren.value) return Promise.resolve(0);
|
|
|
|
|
-
|
|
|
|
|
- return new Promise<number>((resolve) => {
|
|
|
|
|
- // 如果节点已经展开,说明子节点已经显示,可以直接测量,不需要隐藏
|
|
|
|
|
- const needHiding = !isExpanded.value;
|
|
|
|
|
-
|
|
|
|
|
- if (needHiding) {
|
|
|
|
|
- isMeasuringHeight.value = true;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- nextTick(() => {
|
|
|
|
|
- getRect(`#ie-tree-children-${nodeKey.value}`).then((res) => {
|
|
|
|
|
- if (needHiding) {
|
|
|
|
|
- isMeasuringHeight.value = false;
|
|
|
|
|
- }
|
|
|
|
|
- const height = res?.height ?? 0;
|
|
|
|
|
- resolve(height);
|
|
|
|
|
- });
|
|
|
|
|
- });
|
|
|
|
|
- }, 50);
|
|
|
|
|
- });
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 处理节点点击
|
|
|
|
|
-const handleClick = async () => {
|
|
|
|
|
- // 切换展开状态
|
|
|
|
|
- const expandedProp = mergedProps.value.isExpanded;
|
|
|
|
|
- const newExpandedState = !isExpanded.value;
|
|
|
|
|
-
|
|
|
|
|
- if (newExpandedState) {
|
|
|
|
|
- // 展开流程:
|
|
|
|
|
- // 1. 先设置高度为 0(起始状态)
|
|
|
|
|
- measuredHeight.value = 0;
|
|
|
|
|
-
|
|
|
|
|
- // 2. 更新展开状态,让子节点容器渲染
|
|
|
|
|
- props.nodeData[expandedProp] = newExpandedState;
|
|
|
|
|
-
|
|
|
|
|
- // 3. 等待 DOM 更新,确保子节点容器已经渲染出来
|
|
|
|
|
- await nextTick();
|
|
|
|
|
-
|
|
|
|
|
- // 4. 测量目标高度(此时子节点已经渲染,但高度是 0)
|
|
|
|
|
- const height = await measureHeight();
|
|
|
|
|
-
|
|
|
|
|
- // 5. 再等一帧,确保浏览器已经渲染了高度为 0 的状态
|
|
|
|
|
- await nextTick();
|
|
|
|
|
-
|
|
|
|
|
- // 6. 设置目标高度,触发 CSS 动画(0 → height)
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- measuredHeight.value = height;
|
|
|
|
|
- }, 0)
|
|
|
|
|
-
|
|
|
|
|
- // 7. 动画完成后(350ms)设置为 auto
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- if (isExpanded.value && props.nodeData[expandedProp]) {
|
|
|
|
|
- measuredHeight.value = 'auto';
|
|
|
|
|
- }
|
|
|
|
|
- }, 350);
|
|
|
|
|
-
|
|
|
|
|
- } else {
|
|
|
|
|
- // 收起流程:
|
|
|
|
|
- // 1. 如果当前是 auto,先测量实际高度并设置为固定值
|
|
|
|
|
- if (measuredHeight.value === 'auto') {
|
|
|
|
|
- const height = await measureHeight();
|
|
|
|
|
- measuredHeight.value = height;
|
|
|
|
|
- await nextTick();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 2. 等待一帧,确保浏览器已经应用了固定高度
|
|
|
|
|
- await nextTick();
|
|
|
|
|
-
|
|
|
|
|
- // 3. 设置为 0,触发收起动画
|
|
|
|
|
- measuredHeight.value = 0;
|
|
|
|
|
-
|
|
|
|
|
- // 4. 更新展开状态
|
|
|
|
|
- props.nodeData[expandedProp] = newExpandedState;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 触发节点点击事件
|
|
|
|
|
- // emit('node-click', {
|
|
|
|
|
- // node: props.nodeData,
|
|
|
|
|
- // parent: props.parentData || null
|
|
|
|
|
- // });
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// 处理子节点点击事件
|
|
|
|
|
-const handleChildNodeClick = (data: { node: any; parent: any | null }) => {
|
|
|
|
|
- emit('node-click', data);
|
|
|
|
|
-};
|
|
|
|
|
-</script>
|
|
|
|
|
-
|
|
|
|
|
-<style lang="scss" scoped>
|
|
|
|
|
-.ie-tree-node {
|
|
|
|
|
- &__default {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- padding: 10px 0;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__expand-icon {
|
|
|
|
|
- margin-right: 8px;
|
|
|
|
|
- transition: transform 0.3s;
|
|
|
|
|
-
|
|
|
|
|
- &--expanded {
|
|
|
|
|
- transform: rotate(90deg);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__label {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- &__children {
|
|
|
|
|
- padding-left: 20px;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
- transition: height 0.3s ease-in-out;
|
|
|
|
|
- will-change: height;
|
|
|
|
|
-
|
|
|
|
|
- &--measuring {
|
|
|
|
|
- opacity: 0;
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- z-index: -1000;
|
|
|
|
|
- transform: translateX(0);
|
|
|
|
|
- height: auto !important;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-</style>
|
|
|