npm package: @pluve/lego-tree-vue
Description: 乐高系列之 tree 组件
Author: yangwend
Latest version: 0.1.3

# @pluve/lego-tree-vue

乐高系列之 `tree` 组件系列，属于强业务组件。

[![npm (scoped)](https://img.shields.io/npm/v/@pluve/lego-tree-vue?style=for-the-badge)](https://www.npmjs.com/package/@pluve/lego-tree-vue)

`@pluve/lego-tree-vue` 一期二期规划的功能已上线，目前已在【营销工具 - 一元券】等项目中使用。

## 安装

```bash
# 依赖 vue@3.x/ant-design-vue@3.x/@ant-design/icons-vue@6.x/@pluve/lego-excel-vue@*
yarn add @pluve/lego-tree-vue
```

## 公共类型 LegoTreeTypes

#### LegoTreeTypes.OrgTreeJsonFromEnum

>组织机构树 orgTree.json 来源枚举。**当走 SSO 方式时，不区分全量 或者 有效 的数据源**

```tsx
export enum OrgTreeJsonFromEnum {
  SSO = 'sso', // SSO（默认，不区分全量 or 有效）
  USER = 'user', // 账号中心
}
```

#### LegoTreeTypes.OrgTreeJsonTypeEnum

>组织机构树 json 类型：全量(包含已关停等) or 有效的，提供枚举。

```tsx
export enum OrgTreeJsonTypeEnum {
  ALL = 'all', // 全量的数据
  ENABLE = 'enable', // 有效的数据（默认）
}
```

#### LegoTreeTypes.CommonHierarchyEnum

>组织机构树原始树结构返回的 `hierarchy` 字段枚举。

```tsx
export enum CommonHierarchyEnum {
  COMPANY = '01', // 公司
  CENTER = '02', // 中心
  OPERATING_AREA = '03', // 营运区
  DEPARTMENT = '04', // 部门
  GROUP = '05', // 组
  AREA = '06', // 片区
  STORE = '07', // 门店
}
```

#### LegoTreeTypes.IOriginTreeNode

>接口返回的原始节点信息，除 `[key: string]` 外，其他字段都为 通用组织机构树接口 返回的字段。

| 参数 | 说明 | 类型 | 是否可选 |
| --- | --- | --- | --- |
| code | 节点编码 | `string` | 是 |
| hierarchy | （SSO）组织机构级别：01(公司)；02(中心)；03(营运区)；04(部门)；05(组)；06(片区)；07(门店)； | `string` | 是 |
| rank | （账号中心）组织机构级别：01(公司)；02(中心)；03(营运区)；04(部门)；05(组)；06(片区)；07(门店)； | `LegoTreeTypes.CommonHierarchyEnum` | 是 | 
| id | 唯一 key | `string` | 否 |
| name | 名称 | `string` | 是 |
| parentId | 父 id | `string` | 是 |
| storeCode | 门店编码 | `string` | 是 |
| disabled | 是否节点禁用 | `boolean` | 是 |
| selectable | 【单选】节点是否可以选择 | `boolean` | 是 |
| sub | （SSO）子节点 | `LegoTreeTypes.IOriginTreeNode[]` | 是 |
| children | （账号中心）子节点 | `LegoTreeTypes.IOriginTreeNode[]` | 是 |
| `[key: string]` | 其他字段，用于匹配非组织机构树接口的返回字段 | `any` | 是 |

#### LegoTreeTypes.ITreeNodesType

>接口返回数据**转化后**的节点信息，必须按照如下类型来传输。

| 参数 | 说明 | 类型 | 是否可选 |
| --- | --- | --- | --- |
| key | 唯一 `key`。走默认数据源取数逻辑 && 账号中心全量数据时，key 取值方式：code_storeCode；走默认数据源取数逻辑 && SSO 或 账号中心有效数据时，key 取值方式：code/storeCode； | `string` | 否 |
| value | 唯一 `key`，`LegoTreeSelect` 组件使用。走默认数据源取数逻辑 && 账号中心全量数据时，value 取值方式：code_storeCode；走默认数据源取数逻辑 && SSO 或 账号中心有效数据时，value 取值方式：code/storeCode； | `string` | 是 |
| title | 节点名称 | `string` | 是 |
| hierarchy | 组织机构级别：01(公司)；02(中心)；03(营运区)；04(部门)；05(组)；06(片区)；07(门店)；<strong>若接口返回该字段为空，则组件内部处理为从1开始往下叠加</strong> | `string` | 是 |
| code | 原始 `code` | `string` | 是 |
| storeCode | 原始 `storeCode`，门店节点时才存在该字段 | `string` | 是 |
| pid | 上一节点的 'code' 字段 | `string` | 是 |
| trace | 父子节点路径，<strong>`includeArea` 为 `true` 时，该字段必须有值</strong> | `string` | 是 |
| disabled | 是否节点禁止操作 | `boolean` | 是 |
| selectable | 【单选】节点是否可以选择 | `boolean` | 是 |
| children | 子节点 | `LegoTreeTypes.ITreeNodesType[]` | 是 |
| originNode | 原始树节点信息，`keepOriginNode` 开启后 该对象不为 `undefined`，且在 `includeArea` 为 `true` 或 `labelInValue` 为 `true` 时返回给调用方 | `LegoTreeTypes.IOriginTreeNode` | 是 |

#### LegoTreeTypes.ICheckedAreaItem

>可选择父子层级的选中项节点信息

| 参数 | 说明 | 类型 | 是否可选 |
| --- | --- | --- | --- |
| key | 唯一 key。走默认数据源取数逻辑 && 账号中心全量数据时，key 取值方式：code_storeCode；走默认数据源取数逻辑 && SSO 或 账号中心有效数据时，key 取值方式：code/storeCode； | `string` | 否 |
| title | 节点名称 | `string` | 是 |
| hierarchy | 组织机构级别：01(公司)；02(中心)；03(营运区)；04(部门)；05(组)；06(片区)；07(门店)；<strong>若接口返回该字段为空，则组件内部处理为从1开始往下叠加</strong> | `string` | 是 |
| code | 原始 `code` | `string` | 是 |
| storeCode | 原始 `storeCode`，门店节点时才存在该字段 | `string` | 是 |
| originNode | 原始树节点信息，`keepOriginNode` 开启后 该对象不为 `undefined`，且在 `includeArea` 为 `true`时返回给调用方 | `LegoTreeTypes.IOriginTreeNode` | 是 |

#### LegoTreeTypes.ITreeSelectLabelInValue

>LegoTreeSelect 组件 开启 `labelInValue` 时的 `value` 类型，即 当前节点的信息 & antd TreeSelect 组件返回的当前节点的信息的并集

```ts
export interface ITreeSelectLabelInValue extends Partial<LegoTreeTypes.ITreeNodesType> {
  /**
   * @description 走默认数据源取数逻辑 && 账号中心全量数据时，value 取值方式：code_storeCode
   * 走默认数据源取数逻辑 && SSO 或 账号中心有效数据时，value 取值方式：code/storeCode
   * @author yangwen
   * @type {string}
   * @memberof ITreeSelectLabelInValue
   */
  value: string;
  label?: string;
  disabled?: boolean;
}
```

| 参数 | 说明 | 类型 | 是否可选 |
| --- | --- | --- | --- |
| value | 唯一 `key`，`LegoTreeSelect` 组件使用。走默认数据源取数逻辑 && 账号中心全量数据时，value 取值方式：code_storeCode；走默认数据源取数逻辑 && SSO 或 账号中心有效数据时，value 取值方式：code/storeCode； | `string` | 否 |
| label | 节点名称，与 `title` 字段取值一致 | `string` | 是 |
| disabled | 是否节点禁止操作 | `boolean` | 是 |

## 树数据源处理

优先级：treeData > request > orgTree.json

#### 1. treeData
若传值 `treeData` 不为 `undefined`，则组件认定是调用方需要自定义数据源，则完全信任调用方传入的数据源，组件内部不对 `treeData` 做处理。
同时接收 `loading` 字段 和 `dataListMap` 字段，作为加载数据源的 `加载状态` 和 `扁平化的数据源`。
此时需要区分 2 种情况：

1. `dataListMap` 为空对象：则调用 `LegoTreeUtils.generateListMap` 方法来生成 扁平化数据源 `dataListMap` 并存储在组件内部，同时给 `treeData` 子节点增加 `trace` 字段用作记录父子节点的路径。

2. `dataListMap` 不为空对象：则默认调用方已经扁平化了数据源 且给子节点增加了 `trace` 字段，直接存储在组件内部即可。


**注意：**
* **`trace` 字段主要用于组织机构树可以选择任意节点的场景，需要通过 trace 字段来判定父子节点间的关系。如果只需要选择子节点，则可以忽略所有针对 `trace` 字段的要求。**
* `treeData` 里面每个节点的 `originNode` **字段需要自行处理**。`includeArea` 为 `true` 或 `labelInValue` 为 `true` 时，若开启 `keepOriginNode`，则 返回的值里面会增加 `originNode` 字段。

#### 2. request
若传值 `treeData` 为 `undefined`，且 `request` 不为 `undefined`，则组件认定是调用方需要通过**自定义服务**来自定义数据源，则完全信任调用方传入**自定义服务**，组件内部执行调用服务逻辑。
获取数据源后，通过调用 `LegoTreeUtils.generateListMap` 方法来生成 扁平化数据源 `dataListMap` 并存储在组件内部，同时给 `treeData` 子节点增加 `trace` 字段用作记录父子节点的路径。

**注意：**
* **`trace` 字段主要用于组织机构树可以选择任意节点的场景，需要通过 trace 字段来判定父子节点间的关系。如果只需要选择子节点，则可以忽略所有针对 `trace` 字段的要求。**
* `treeData` 里面每个节点的 `originNode` **字段需要自行处理**。`includeArea` 为 `true` 或 `labelInValue` 为 `true` 时，若开启 `keepOriginNode`，则 返回的值里面会增加 `originNode` 字段。

#### 3. orgTree.json
若上述 2 个字段都为 `undefined`，则组件认定调用方没有自定义的需求，则直接走组件默认的数据源取数逻辑：

1. 根据 `envType` 字段区分走测试还是生产环境的接口调用——**注意：此时获取到的数据没有做任何权限处理**
2. 根据 `jsonFrom` 字段区分走 SSO 还是走账号中心——**注意：SSO 只返回有效的数据源，不包含已关停数据，如需已关停数据，请走账号中心**
3. 根据 `jsonType` 字段区分返回全量(包含已关停等)还是有效的数据源，获取全部的组织机构树的数据源
4. 根据 `hierarchyList` 字段过滤不包含的节点信息，传空或不传则默认取全量数据源
5. 调用 `LegoTreeUtils.recursionTreeData` 方法对数据源做处理，转化为 `Tree/TreeSelect` 组件需要的数据源
6. 根据 `selectHierarchy` 字段过滤子节点不包含 `selectHierarchy` 层级的节点。例如，不包含门店节点的父节点全部过滤掉不展示，则 `selectHierarchy` 为 '07'。
7. 调用 `LegoTreeUtils.generateListMap` 方法来生成 扁平化数据源 `dataListMap` 并存储在组件内部，同时给 `treeData` 子节点增加 `trace` 字段用作记录父子节点的路径，并更新 `treeData` 且展示在树上。

#### 相关源码

```tsx
const loadOrgTreeNodeByLevel = async () => {
  if (state.loading) {
    return;
  }

  state.loading = true;

  // 此处 使用 toRaw 针对 props.treeData/props.dataListMap（响应式对象） 做转化
  if (Object.prototype.toString.call(props.treeData) === '[object Array]') {
    // 此处需要确保传的 treeData 已经做了数据处理且包含 trace 字段
    state.treeData = toRaw(props.treeData || []);
    // dataListMap 有值则直接取
    if (Object.values(toRaw(props.dataListMap || {})).length) {
      // 此处需要确保传的 dataListMap 已经做了数据处理
      state.dataListMap = toRaw(props.dataListMap || {});
    } else {
      // dataListMap 没有值则计算再赋值
      const { dataList, dataListMap } = LegoTreeUtils.generateListMap(
        toRaw(props.treeData || [])
      );
      // 此时 treeData 增加了 trace 字段
      state.treeData = dataList;
      state.dataListMap = dataListMap;
    }

    state.loading = false;
    // 接口请求成功后，往外触发 getTreeInfo 事件，获取树相关数据
    emit('getTreeInfo', {
      treeData: toRaw(state.treeData),
      dataListMap: toRaw(state.dataListMap),
    });
    return;
  }

  LegoTreeUtils.loadOrgTreeNodeByLevel(
    {
      request: props.request,
      selectHierarchy: props.selectHierarchy,
      envType: props.envType,
      jsonFrom: props.jsonFrom,
      jsonType: props.jsonType,
      hierarchyList: props.hierarchyList,
      keepOriginNode: props.keepOriginNode ?? false,
    },
    (dataList, dataListMap) => {
      state.treeData = dataList;
      state.dataListMap = dataListMap;
      state.loading = false;
      // 接口请求成功后，往外触发 getTreeInfo 事件，获取树相关数据
      emit('getTreeInfo', {
        treeData: dataList,
        dataListMap: dataListMap,
      });
    }
  );
};
```

```ts
const loadOrgTreeNodeByLevel = async (
  {
    request,
    selectHierarchy = '',
    envType = 'test',
    jsonFrom = LegoTreeTypes.OrgTreeJsonFromEnum.SSO,
    jsonType = LegoTreeTypes.OrgTreeJsonTypeEnum.ENABLE,
    hierarchyList = [],
    enableSelectedHierarchy = [],
    keepOriginNode = false,
  }: {
    request?: () => Promise<LegoTreeTypes.ITreeNodesType[]>; // 自定义 request 接口请求
    selectHierarchy?: string; // 选择的组织机构级别：01(公司)；02(中心)；03(营运区)；04(部门)；05(组)；06(片区)；07(门店)；
    envType?: 'test' | 'prod'; // 环境（测试 | 生产）
    jsonFrom?: LegoTreeTypes.OrgTreeJsonFromEnum; // 组织机构树 orgTree.json 来源
    jsonType?: LegoTreeTypes.OrgTreeJsonTypeEnum; // 组织机构树 json 类型：全量(包含已关停等) or 有效的
    hierarchyList?: string[]; // 组织机构级别数组，默认全部
    enableSelectedHierarchy?: string[]; // 【单选】可选择的组织机构级别，不传或为空则都可以选择，传了则包含的级别才可以选择
    keepOriginNode?: boolean; // 是否树节点保留原始对象信息
  },
  callback: (
    treeData: LegoTreeTypes.ITreeNodesType[],
    dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>
  ) => void
) => {
  try {
    let newTreeData: LegoTreeTypes.ITreeNodesType[] = [];
    // 直接传接口进来，则直接调接口，接口中已经对数据做了处理
    if (typeof request !== 'undefined') {
      newTreeData = await request();
    } else {
      // 否则走默认接口，组件中做处理
      const result = await LegoTreeServices.getAllOrgTreeNodesByOss({
        hierarchyList,
        envType,
        jsonFrom,
        jsonType,
      });
      // 将接口返回的数据结构转化为 TreeSelect/Tree 组件需要的数据结构
      newTreeData = LegoTreeUtils.recursionTreeData({
        list:
          result?.[0][jsonFrom === LegoTreeTypes.OrgTreeJsonFromEnum.SSO ? 'sub' : 'children'] ||
          [],
        childrenKey: jsonFrom === LegoTreeTypes.OrgTreeJsonFromEnum.SSO ? 'sub' : 'children',
        hierarchyKey: jsonFrom === LegoTreeTypes.OrgTreeJsonFromEnum.SSO ? 'hierarchy' : 'rank',
        pid: undefined,
        enableSelectedHierarchy, // 【单选】可选择的组织机构级别，不传或为空则都可以选择，传了则包含的级别才可以选择
        keepOriginNode,
        includeDisabledData: LegoTreeUtils.isAllIncludeDisabledData({ jsonFrom, jsonType }),
      });
      // 过滤树底层没有 selectHierarchy 的节点
      LegoTreeUtils.excludeNode(newTreeData, selectHierarchy);
    }

    const { dataList, dataListMap } = LegoTreeUtils.generateListMap(newTreeData);
    callback(dataList, dataListMap);
  } catch (error) {
    callback([], {});
  }
};
```

## LegoTreeSelect

通常用于单选组织架构，例如选择门店、公司、各组织机构等。`props` 继承自 antd vue `TreeSelectProps`。

```ts
export interface ILegoTreeSelectProps
  extends Omit<
    TreeSelectProps,
    | 'defaultValue'
    | 'fieldNames'
    | 'multiple'
    | 'replaceFields'
    | 'searchValue'
    | 'showSearch'
    | 'treeCheckable'
    | 'treeCheckStrictly'
    | 'treeData'
    | 'treeDataSimpleMode'
    | 'virtual'
    | 'onSearch'
    | 'loading'
    | 'value'
    | 'onChange'
  > {
  envType?: 'test' | 'prod';
  jsonFrom?: OrgTreeJsonFromEnum;
  jsonType?: OrgTreeJsonTypeEnum;
  hierarchyList?: string[];
  selectHierarchy?: string;
  enableSelectedHierarchy?: string[];

  treeData?: LegoTreeTypes.ITreeNodesType[];
  loading?: boolean;
  dataListMap?: Record<string, LegoTreeTypes.ITreeNodesType>;
  request?: () => Promise<LegoTreeTypes.ITreeNodesType[]>;

  defaultValue?: string | LegoTreeTypes.ITreeSelectLabelInValue;
  value?: string | LegoTreeTypes.ITreeSelectLabelInValue;

  keepOriginNode?: boolean;
}
```

### API
| 参数 | 说明 | 类型 | 是否可选 | 默认值 |
| --- | --- | --- | --- | --- |
| envType | 环境（测试 \| 生产）。走组件默认接口请求时使用，其余场景无需传该字段 | `test \| prod` | 是 | `test` |
| jsonFrom | 组织机构树 json 来源。走组件默认接口请求时使用，其余场景无需传该字段 | `LegoTreeTypes.OrgTreeJsonFromEnum` | 是 | `LegoTreeTypes.OrgTreeJsonFromEnum.SSO` |
| jsonType | 组织机构树 json 类型：全量(包含已关停等) or 有效的。走组件默认接口请求时使用，其余场景无需传该字段 | `LegoTreeTypes.OrgTreeJsonTypeEnum` | 是 | `LegoTreeTypes.OrgTreeJsonTypeEnum.ENABLE` |
| hierarchyList | 组织机构级别数组，01-07，用于过滤不包含的节点信息，没传或为空则不过滤。走组件默认接口请求时使用，其余场景无需传该字段 | `string[]` | 是 | - |
| selectHierarchy | 选择的组织机构级别，01-07，用于过滤子节点不包含 `selectHierarchy` 层级的节点。走组件默认接口请求时使用，其余场景无需传该字段 | `string` | 是 | - |
| enableSelectedHierarchy | 【单选】可选择的组织机构级别，不传或为空则都可以选择，传了则包含的级别才可以选择，否则 `selectable` 为 `false` 不可选择 | `string[]` | 是 | [] |
| treeData | antd vue `TreeSelect` 组件数据源（已经经过转化后的数据源） | `LegoTreeTypes.ITreeNodesType[]` | 是 | - |
| loading | 外部数据源加载时的 `loading` 状态 | `boolean` | 是 | `false` |
| dataListMap | 扁平化 Map（已经经过转化后的数据源生成的 map） | `Record<string, LegoTreeTypes.ITreeNodesType>` | 是 | {} |
| request | 自定义服务，用于获取组件树数据源 | `() => Promise<LegoTreeTypes.ITreeNodesType[]>` | 是 | - |
| defaultValue | 默认选中的值 | `string \| LegoTreeTypes.ITreeSelectLabelInValue` | 是 | - |
| value(v-model) | 当前选中的值 | `string \| LegoTreeTypes.ITreeSelectLabelInValue` | 是 | - |
| keepOriginNode | 是否树节点保留原始对象信息。若开启，配合 `labelInValue` 字段为 `true`，则会返回 `originNode`（原始对象信息） | `boolean` | 是 | `false` |
| title | 自定义标题，在原始基础上，增加当前搜索值 `searchValue` 字段，供自定义判断处理 | `slot({ key, title, ..., searchValue })` | 是 | - |

### 事件(emit)
| 事件名称 | 说明 | 回调参数 |
| --- | --- | --- |
| change | 选中树节点时调用此函数 | `function(string \| LegoTreeTypes.ITreeSelectLabelInValue)` |
| getTreeInfo | 接口请求完成后，调用此函数，返回当前组件内部的树数据源 | `function({ treeData: LegoTreeTypes.ITreeNodesType[]; dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>; })` |

### ref

通过 ref 获取组件提供的方法或属性。以下是提供的方法或属性：
| 方法/属性名称 | 说明 | 类型 |
| --- | --- | --- |
| getTreeInfo | 【方法】提供组件内部的树数据源 | `() => ({ treeData: LegoTreeTypes.ITreeNodesType[]; dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>; })` |

### 效果展示

测试环境地址：https://yf-test-oss.yifengx.com/webtest/yangwen/legoTree/index.html#/treeSelect

### 使用示例

```vue
<template>
  <ConfigProvider :locale="zh_CN">
    <div :class="$style.wrap">
      <Form :colon="false" autocomplete="off">
        <Row :gutter="20">
          <!-- 有效数据源 -->
          <Col :span="12">
            <FormItem label="labelInValue true" v-bind="form.validateInfos.rangeCodeObj">
              <LegoTreeSelect
                :hierarchyList="['01', '02', '03', '04', '06', '07']"
                selectHierarchy="07"
                :enable-selected-hierarchy="['01', '07']"
                v-model:value="modelRef.rangeCodeObj"
                :labelInValue="true"
                :default-value="{ value: 'H535' }"
                :keep-origin-node="true"
                ref="treeRef"
                @get-tree-info="getTreeInfo"
              >
                <template #title="{ value: val, title, label, storeCode }">
                  <b v-if="val === '5024'" style="color: #08c">{{ title || label }}</b>
                  <b v-else-if="storeCode === '5500'" style="color: #f90">{{ title || label }}</b>
                  <template v-else>{{ title || label }}</template>
                </template>
              </LegoTreeSelect>
            </FormItem>
          </Col>
          <Col :span="12">
            <FormItem label="labelInValue false" v-bind="form.validateInfos.rangeCode">
              <LegoTreeSelect
                :treeData="state.treeData"
                :loading="state.loading"
                v-model:value="modelRef.rangeCode"
                :labelInValue="false"
              ></LegoTreeSelect>
            </FormItem>
          </Col>

          <!-- 账号中心 && 全部数据源（包含已关停） -->
          <Col :span="24" style="font-weight: bold; color: red; margin-bottom: 24px">
            以下数据源为账号中心全量数据源（包含已关停数据）：
          </Col>
          <Col :span="12">
            <FormItem label="labelInValue true" v-bind="form.validateInfos.rangeCodeObjAll">
              <LegoTreeSelect
                :json-from="LegoTreeTypes.OrgTreeJsonFromEnum.USER"
                :json-type="LegoTreeTypes.OrgTreeJsonTypeEnum.ALL"
                :hierarchyList="['01', '02', '03', '04', '06', '07']"
                selectHierarchy="07"
                :enable-selected-hierarchy="['01', '07']"
                v-model:value="modelRef.rangeCodeObjAll"
                :labelInValue="true"
                :default-value="{ value: '10025961_H535' }"
                :keep-origin-node="true"
                ref="treeRefAll"
                @get-tree-info="getTreeInfoAll"
              >
                <template #title="{ value: val, title, label, storeCode }">
                  <b v-if="val === '10004575_5024'" style="color: #08c">{{ title || label }}</b>
                  <b v-else-if="storeCode === '5500'" style="color: #f90">{{ title || label }}</b>
                  <template v-else>{{ title || label }}</template>
                </template>
              </LegoTreeSelect>
            </FormItem>
          </Col>
          <Col :span="12">
            <FormItem label="labelInValue false" v-bind="form.validateInfos.rangeCodeAll">
              <LegoTreeSelect
                :treeData="state.treeDataAll"
                :loading="state.loadingAll"
                v-model:value="modelRef.rangeCodeAll"
                :labelInValue="false"
              ></LegoTreeSelect>
            </FormItem>
          </Col>
          <Col :span="6">
            <Button @click="onQuery" style="margin-left: 10px" type="primary">查询</Button>
          </Col>
        </Row>
      </Form>
    </div>
  </ConfigProvider>
</template>

<script setup lang="ts">
import zh_CN from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs';
import type { Rule } from 'ant-design-vue/es/form';
import { ConfigProvider, Form, FormItem, Row, Col, Button } from 'ant-design-vue';
import { reactive, onMounted, ref } from 'vue';
import { LegoTreeSelect, LegoTreeUtils, LegoTreeTypes, LegoTreeServices } from '@pluve/lego-tree-vue';
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');

const treeRef = ref();
const treeRefAll = ref();

// 查询条件 ref
const modelRef = reactive<{
  rangeCodeObj?: LegoTreeTypes.ITreeSelectLabelInValue;
  rangeCodeObjAll?: LegoTreeTypes.ITreeSelectLabelInValue;
  rangeCode?: string;
  rangeCodeAll?: string;
}>({
  rangeCodeObj: undefined,
  rangeCodeObjAll: undefined,
  rangeCode: '5024',
  rangeCodeAll: '10004575_5024', // 全量数据源时：需要使用 code_storeCode 格式
});

const rulesRef: Record<string, Rule[]> = reactive({
  rangeCodeObj: [
    {
      required: true,
      validator: (_rule: Rule, value?: LegoTreeTypes.ITreeSelectLabelInValue) => {
        if (!value) {
          return Promise.reject(new Error('请选择'));
        }
        return Promise.resolve();
      },
    },
  ],
  rangeCodeObjAll: [
    {
      required: false,
    },
  ],
  rangeCode: [
    {
      required: false,
    },
  ],
  rangeCodeAll: [
    {
      required: true,
      validator: (_rule: Rule, value?: string) => {
        if (!value) {
          return Promise.reject(new Error('请选择'));
        }
        return Promise.resolve();
      },
    },
  ],
});

// 查询条件 form
const form = Form.useForm(modelRef, rulesRef);

const state = reactive<{
  treeData: LegoTreeTypes.ITreeNodesType[];
  treeDataAll: LegoTreeTypes.ITreeNodesType[];
  loading: boolean;
  loadingAll: boolean;
}>({
  treeData: [],
  treeDataAll: [],
  loading: false,
  loadingAll: false,
});

// 获取 SSO 有效数据源
const loadOrgTreeNodeByLevel = async () => {
  state.loading = true;
  try {
    const result = await LegoTreeServices.getAllOrgTreeNodesByOss({
      hierarchyList: ['01', '02', '03', '04', '06', '07'],
      envType: 'test',
    });
    const newTreeData = LegoTreeUtils.recursionTreeData({
      list: result?.[0].sub || [],
      pid: undefined,
    });
    LegoTreeUtils.excludeNode(newTreeData, '07');
    state.treeData = newTreeData;
  } catch (error) {
    state.treeData = [];
  } finally {
    state.loading = false;
  }
};

// 获取账号中心全量数据源
const loadOrgTreeNodeByLevelAll = async () => {
  state.loadingAll = true;
  try {
    const result = await LegoTreeServices.getAllOrgTreeNodesByOss({
      hierarchyList: ['01', '02', '03', '04', '06', '07'],
      envType: 'test',
      jsonFrom: LegoTreeTypes.OrgTreeJsonFromEnum.USER,
      jsonType: LegoTreeTypes.OrgTreeJsonTypeEnum.ALL,
    });
    const newTreeData = LegoTreeUtils.recursionTreeData({
      list: result?.[0].children || [],
      pid: undefined,
      childrenKey: 'children',
      hierarchyKey: 'rank',
      includeDisabledData: true,
    });
    LegoTreeUtils.excludeNode(newTreeData, '07');
    state.treeDataAll = newTreeData;
  } catch (error) {
    state.treeDataAll = [];
  } finally {
    state.loadingAll = false;
  }
};

onMounted(() => {
  loadOrgTreeNodeByLevel();
  loadOrgTreeNodeByLevelAll();
});

const onQuery = () => {
  console.log('treeRef.value', treeRef.value?.getTreeInfo());
  console.log('treeRefAll.value', treeRefAll.value?.getTreeInfo());

  form
    .validate()
    .then(() => {
      console.log('查询条件：', modelRef);
    })
    .catch((err) => {
      console.log('err->', err);
    });
};

// 接口请求成功后返回对应的树数据
const getTreeInfo = ({
  treeData,
  dataListMap,
}: {
  treeData: LegoTreeTypes.ITreeNodesType[];
  dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>;
}) => {
  console.log('getTreeInfo', { treeData, dataListMap });
};

// 接口请求成功后返回对应的树数据
const getTreeInfoAll = ({
  treeData,
  dataListMap,
}: {
  treeData: LegoTreeTypes.ITreeNodesType[];
  dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>;
}) => {
  console.log('getTreeInfoAll', { treeData, dataListMap });
};
</script>

<style lang="less" module>
.wrap {
  position: relative;
  background-color: #fff;
  padding: 40px 80px;
  border-radius: 4px;
}
</style>
```

## LegoLeftTree

通常用于页面左右侧布局，左侧展示组织机构树，右侧展示业务内容。左侧操作后右侧同步更新。

```ts
export interface ILegoLeftTreeProps
  extends Omit<
    TreeProps,
    | 'blockNode'
    | 'fieldNames'
    | 'multiple'
    | 'treeData'
    | 'virtual'
    | 'checkable'
    | 'selectable'
    | 'checkedKeys'
    | 'selectedKeys'
    | 'autoExpandParent'
    | 'expandedKeys'
    | 'height'
  > {
  envType?: 'test' | 'prod';
  jsonFrom?: OrgTreeJsonFromEnum;
  jsonType?: OrgTreeJsonTypeEnum;
  hierarchyList?: string[];
  selectHierarchy?: string;
  enableSelectedHierarchy?: string[];

  treeData?: LegoTreeTypes.ITreeNodesType[];
  loading?: boolean;
  dataListMap?: Record<string, LegoTreeTypes.ITreeNodesType>;
  request?: () => Promise<LegoTreeTypes.ITreeNodesType[]>;

  value?: string | string[] | LegoTreeTypes.ICheckedAreaItem[];
  headerTitle?: string;
  placeholder?: string;
  disabled?: boolean;
  selectType?: 'single' | 'multiple';
  includeArea?: boolean;
  width?: number | string;
  virtualHeight?: number;
  showSwitch?: boolean;
  defaultIsShow?: boolean;

  keepOriginNode?: boolean;
}
```

### API
| 参数 | 说明 | 类型 | 是否可选 | 默认值 |
| --- | --- | --- | --- | --- |
| envType | 环境（测试 \| 生产）。走组件默认接口请求时使用，其余场景无需传该字段 | `test \| prod` | 是 | `test` |
| jsonFrom | 组织机构树 json 来源。走组件默认接口请求时使用，其余场景无需传该字段 | `LegoTreeTypes.OrgTreeJsonFromEnum` | 是 | `LegoTreeTypes.OrgTreeJsonFromEnum.SSO` |
| jsonType | 组织机构树 json 类型：全量(包含已关停等) or 有效的。走组件默认接口请求时使用，其余场景无需传该字段 | `LegoTreeTypes.OrgTreeJsonTypeEnum` | 是 | `LegoTreeTypes.OrgTreeJsonTypeEnum.ENABLE` |
| hierarchyList | 组织机构级别数组，01-07，用于过滤不包含的节点信息，没传或为空则不过滤。走组件默认接口请求时使用，其余场景无需传该字段 | `string[]` | 是 | - |
| selectHierarchy | 选择的组织机构级别，01-07，用于过滤子节点不包含 `selectHierarchy` 层级的节点。走组件默认接口请求时使用，其余场景无需传该字段 | `string` | 是 | - |
| enableSelectedHierarchy | 【单选】可选择的组织机构级别，不传或为空则都可以选择，传了则包含的级别才可以选择，否则 `selectable` 为 `false` 不可选择 | `string[]` | 是 | [] |
| treeData | antd vue `TreeSelect` 组件数据源（已经经过转化后的数据源） | `LegoTreeTypes.ITreeNodesType[]` | 是 | - |
| loading | 外部数据源加载时的 `loading` 状态 | `boolean` | 是 | `false` |
| dataListMap | 扁平化 Map（已经经过转化后的数据源生成的 map） | `Record<string, LegoTreeTypes.ITreeNodesType>` | 是 | {} |
| request | 自定义服务，用于获取组件树数据源 | `() => Promise<LegoTreeTypes.ITreeNodesType[]>` | 是 | - |
| value(v-model) | 当前选中的值，区分单选、多选门店、多选节点三种类型 | `string \| string[] \| LegoTreeTypes.ICheckedAreaItem[]` | 是 | - |
| headerTitle | 头部标题文案 | `string` | 是 | 选择门店 |
| placeholder | Input.Search 输入框 placeholder | `string` | 是 | 搜索门店名称或编码 |
| disabled | tree 选择是否禁用 | `boolean` | 是 | `false` |
| selectType | 选择类型：单选/多选 | `single \| multiple` | 是 | 'single' |
| includeArea | 【多选】**是否包含区域选择，即是否允许选择各层级父子节点** | `boolean` | 是 | false |
| width | 组件宽度 | `string \| number` | 是 | 280 |
| virtualHeight | 组件 Tree 虚拟滚动高度 | `number` | 是 | 820 |
| showSwitch | 是否展示 展开收起功能 开关 | `boolean` | 是 | `true` |
| defaultIsShow | 默认是否处于展开状态 | `boolean` | 是 | `true` |
| keepOriginNode | 是否树节点保留原始对象信息。若开启，配合 `includeArea` 为 `true`，则会返回 `originNode`（原始对象信息） | `boolean` | 是 | `false` |
| title | 自定义标题，在原始基础上，增加当前搜索值 `searchValue` 字段，供自定义判断处理 | `slot({ key, title, ..., searchValue })` | 是 | - |

### 事件(emit)
| 事件名称 | 说明 | 回调参数 |
| --- | --- | --- |
| change | 选中/反选树节点时调用此函数 | `function(string \| string[] \| LegoTreeTypes.ICheckedAreaItem[])` |
| switch | 展开/收起按钮切换时调用此函数 | `function(boolean)` |
| checkBefore | `Tree` `onCheck` 触发时优先调用此函数，此函数第三个参数返回 组件内部的树数据源 | `function(checked: Key[] \| { checked: Key[]; halfChecked: Key[]; }, info: CheckInfo, treeInfo: { treeData: LegoTreeTypes.ITreeNodesType[]; dataListMap: Record<string, LegoTreeTypes.ITreeNodesType> })` |
| getTreeInfo | 接口请求完成后，调用此函数，返回当前组件内部的树数据源 | `function({ treeData: LegoTreeTypes.ITreeNodesType[]; dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>; })` |

### ref

通过 ref 获取组件提供的方法或属性。以下是提供的方法或属性：
| 方法/属性名称 | 说明 | 类型 |
| --- | --- | --- |
| getTreeInfo | 【方法】提供组件内部的树数据源 | `() => ({ treeData: LegoTreeTypes.ITreeNodesType[]; dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>; })` |

### 效果展示

测试环境地址：https://yf-test-oss.yifengx.com/webtest/yangwen/legoTree/index.html#/leftTree

### 使用示例

```vue
<template>
  <div :class="$style.top">
    <RadioGroup v-model:value="state.dataType">
      <Radio value="all">全量（包含已关停）</Radio>
      <Radio value="enable">有效</Radio>
    </RadioGroup>
    <Button type="primary" size="small" @click="getTreeInfoWithRef" style="margin-left: 50px">
      点我获取tree数据源
    </Button>
  </div>

  <!-- 有效数据源 -->
  <div :class="$style.wrap" :style="{ display: state.dataType === 'all' ? 'none' : 'flex' }">
    <LegoLeftTree
      env-type="prod"
      :hierarchyList="['01', '02', '03', '04', '06', '07']"
      selectHierarchy="07"
      :enable-selected-hierarchy="['01', '07']"
      header-title="选择范围"
      placeholder="搜索范围名称或编码"
      v-model:value="state.storeCode"
      :show-switch="false"
      ref="treeRef"
      @get-tree-info="getTreeInfo"
      @change="(val) => onChange1(val)"
    >
      <template #title="{ value: val, title, label }">
        <div>
          <span style="margin-right: 10px">{{ title || label }}</span>
          <exclamation-circle-outlined @click="onIconClick1(val)" />
        </div>
      </template>
    </LegoLeftTree>

    <div style="margin-left: 20px; width: auto; height: 100%">
      <LegoLeftTree
        :request="customRequest"
        v-model:value="state.storeCodes"
        header-title="选择渠道"
        placeholder="搜索渠道名称或编码"
        select-type="multiple"
        width="300px"
        :default-is-show="true"
        @switch="onSwitch2"
        @change="(val) => onChange2(val)"
      >
        <template #title="{ value: val, title, label, searchValue }">
          <div>
            <span
              style="margin-right: 10px"
              :style="{ color: searchValue === val ? 'red' : '#222222' }"
            >
              {{ title || label }}
            </span>
            <up-circle-outlined @click="onIconClick2(val)" />
          </div>
        </template>
      </LegoLeftTree>
    </div>

    <div style="margin-left: 20px; width: auto; height: 100%">
      <LegoLeftTree
        :treeData="state.treeData"
        :loading="state.loading"
        v-model:value="state.storeNodes"
        header-title="选择组织机构"
        select-type="multiple"
        include-area
        :virtual-height="600"
        :default-is-show="false"
        :keep-origin-node="true"
        @switch="onSwitch3"
        @change="(val) => onChange3(val)"
        @check-before="onCheckBefore"
      />
    </div>

    <div :class="$style.right">我是右侧业务内容</div>
  </div>

  <!-- 账号中心 && 全部数据源（包含已关停） -->
  <div :class="$style.wrap" :style="{ display: state.dataType === 'all' ? 'flex' : 'none' }">
    <LegoLeftTree
      :json-from="LegoTreeTypes.OrgTreeJsonFromEnum.USER"
      :json-type="LegoTreeTypes.OrgTreeJsonTypeEnum.ALL"
      env-type="test"
      :hierarchyList="['01', '02', '03', '04', '06', '07']"
      selectHierarchy="07"
      :enable-selected-hierarchy="['01', '07']"
      header-title="选择范围"
      placeholder="搜索范围名称或编码"
      v-model:value="state.storeCodeAll"
      :show-switch="false"
      ref="treeRefAll"
      @get-tree-info="getTreeInfoAll"
      @change="(val) => onChange1(val)"
    >
      <template #title="{ value: val, title, label }">
        <div>
          <span style="margin-right: 10px">{{ title || label }}</span>
          <exclamation-circle-outlined @click="onIconClick1(val)" />
        </div>
      </template>
    </LegoLeftTree>

    <div style="margin-left: 20px; width: auto; height: 100%">
      <LegoLeftTree
        :treeData="state.treeDataAll"
        :loading="state.loadingAll"
        v-model:value="state.storeCodesAll"
        header-title="选择渠道"
        placeholder="搜索渠道名称或编码"
        select-type="multiple"
        width="300px"
        :default-is-show="true"
        @switch="onSwitch2"
        @change="(val) => onChange2(val)"
      >
        <template #title="{ value: val, title, label, searchValue }">
          <div>
            <span
              style="margin-right: 10px"
              :style="{ color: searchValue === LegoTreeUtils.getRealKey(val) ? 'red' : '#222222' }"
            >
              {{ title || label }}
            </span>
            <up-circle-outlined @click="onIconClick2(val)" />
          </div>
        </template>
      </LegoLeftTree>
    </div>

    <div style="margin-left: 20px; width: auto; height: 100%">
      <LegoLeftTree
        :treeData="state.treeDataAll"
        :loading="state.loadingAll"
        v-model:value="state.storeNodesAll"
        header-title="选择组织机构"
        select-type="multiple"
        include-area
        :virtual-height="600"
        :default-is-show="false"
        :keep-origin-node="true"
        @switch="onSwitch3"
        @change="(val) => onChange3(val)"
        @check-before="onCheckBeforeAll"
      />
    </div>

    <div :class="$style.right">我是右侧业务内容</div>
  </div>
</template>

<script setup lang="ts">
import { UpCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue';
import zh_CN from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs';
import { ConfigProvider, Button, RadioGroup, Radio } from 'ant-design-vue';
import { reactive, onMounted, ref } from 'vue';
import { CheckInfo } from 'ant-design-vue/es/vc-tree/props';
import { LegoLeftTree, LegoTreeUtils, LegoTreeTypes, LegoTreeServices } from '@pluve/lego-tree-vue';
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
import { originList } from '@/constant/channel';

const treeRef = ref();
const treeRefAll = ref();

const state = reactive<{
  dataType: 'all' | 'enable';
  treeData: LegoTreeTypes.ITreeNodesType[];
  treeDataAll: LegoTreeTypes.ITreeNodesType[];
  loading: boolean;
  loadingAll: boolean;
  storeCode?: string;
  storeCodeAll?: string;
  storeCodes: string[];
  storeCodesAll: string[];
  storeNodes: LegoTreeTypes.ICheckedAreaItem[];
  storeNodesAll: LegoTreeTypes.ICheckedAreaItem[];
}>({
  dataType: 'enable',
  treeData: [],
  treeDataAll: [],
  loading: false,
  loadingAll: false,
  storeCode: '5500',
  storeCodeAll: '10010357_5500',
  storeCodes: ['5500'],
  storeCodesAll: ['10010357_5500'],
  storeNodes: [{ key: '5500', hierarchy: '07' }],
  storeNodesAll: [{ key: '10010357_5500', code: '10010357', storeCode: '5500', hierarchy: '07' }],
});

const customRequest = async () => {
  try {
    const result: any[] = await new Promise((resolve) => {
      setTimeout(() => {
        resolve(originList);
      }, 800);
    });
    const convertedData = LegoTreeUtils.recursionTreeData({
      list: result ?? [],
      valKey: 'id',
      nameKey: 'text',
      childrenKey: 'children',
    });
    return convertedData;
  } catch (error) {
    return [];
  }
};

const onChange1 = (val: string) => {
  console.log('单选选择范围 val：', val);
};

const onChange2 = (val: string[]) => {
  console.log('多选选择渠道 val：', val);
};

const onChange3 = (val: LegoTreeTypes.ICheckedAreaItem[]) => {
  console.log('多选选择组织机构 val：', val);
};

const onSwitch2 = (bool: boolean) => {
  console.log('多选选择渠道，当前展开收起状态：', bool);
};

const onSwitch3 = (bool: boolean) => {
  console.log('多选选择组织机构，当前展开收起状态：', bool);
};

const onIconClick1 = (val: string) => {
  console.log('单选选择范围，点击图标后获取对应的 value：', val);
};

const onIconClick2 = (val: string) => {
  console.log('多选选择渠道，点击图标后获取对应的 value：', val);
};

const loadOrgTreeNodeByLevel = async () => {
  state.loading = true;
  try {
    const result = await LegoTreeServices.getAllOrgTreeNodesByOss({
      hierarchyList: ['01', '02', '03', '04', '06', '07'],
      envType: 'test',
    });
    const newTreeData = LegoTreeUtils.recursionTreeData({
      list: result?.[0].sub || [],
      pid: undefined,
      keepOriginNode: true,
    });
    LegoTreeUtils.excludeNode(newTreeData, '07');
    state.treeData = newTreeData;
  } catch (error) {
    state.treeData = [];
  } finally {
    state.loading = false;
  }
};

const loadOrgTreeNodeByLevelAll = async () => {
  state.loadingAll = true;
  try {
    const result = await LegoTreeServices.getAllOrgTreeNodesByOss({
      hierarchyList: ['01', '02', '03', '04', '06', '07'],
      envType: 'test',
      jsonFrom: LegoTreeTypes.OrgTreeJsonFromEnum.USER,
      jsonType: LegoTreeTypes.OrgTreeJsonTypeEnum.ALL,
    });
    const newTreeData = LegoTreeUtils.recursionTreeData({
      list: result?.[0].children || [],
      pid: undefined,
      childrenKey: 'children',
      hierarchyKey: 'rank',
      keepOriginNode: true,
      includeDisabledData: true,
    });
    LegoTreeUtils.excludeNode(newTreeData, '07');
    state.treeDataAll = newTreeData;
  } catch (error) {
    state.treeDataAll = [];
  } finally {
    state.loadingAll = false;
  }
};

onMounted(() => {
  loadOrgTreeNodeByLevel();
  loadOrgTreeNodeByLevelAll();
});

// 接口请求成功后返回对应的树数据
const getTreeInfo = ({
  treeData,
  dataListMap,
}: {
  treeData: LegoTreeTypes.ITreeNodesType[];
  dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>;
}) => {
  console.log('getTreeInfo', { treeData, dataListMap });
};

// 接口请求成功后返回对应的树数据
const getTreeInfoAll = ({
  treeData,
  dataListMap,
}: {
  treeData: LegoTreeTypes.ITreeNodesType[];
  dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>;
}) => {
  console.log('getTreeInfoAll', { treeData, dataListMap });
};

const getTreeInfoWithRef = () => {
  if (state.dataType === 'enable') {
    console.log('treeRef.value', treeRef.value?.getTreeInfo());
  } else {
    console.log('treeRefAll.value', treeRefAll.value?.getTreeInfo());
  }
};

const onCheckBefore = (
  _: any,
  e: CheckInfo,
  treeInfo: {
    treeData: LegoTreeTypes.ITreeNodesType[];
    dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>;
  }
) => {
  console.log('treeInfo', treeInfo);
};

const onCheckBeforeAll = (
  _: any,
  e: CheckInfo,
  treeInfo: {
    treeData: LegoTreeTypes.ITreeNodesType[];
    dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>;
  }
) => {
  console.log('treeInfoAll', treeInfo);
};
</script>

<style lang="less" module>
body {
  background-color: #f1f4f5;
  width: 100%;
  height: 100%;
}

.top {
  position: relative;
  padding: 10px 50px 0;
}

.wrap {
  position: relative;
  min-height: 52px;
  background-color: #f1f4f5;
  padding: 10px 50px;
  border-radius: 4px;
  height: calc(100vh - 120px);
  display: flex;
  align-items: center;
  justify-content: flex-start;
}

.right {
  width: 300px;
  height: 100%;
  background: #fff;
  margin-left: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>
```

## LegoTreeTransfer

通常作为最底层组件向外提供，可以嵌在 tab 内、Modal 弹窗内等任何场景，左右侧布局展示。`props` 继承自 antd vue `TreeProps`。

```ts
export interface ILegoTreeTransferProps
  extends Omit<
    TreeProps,
    | 'blockNode'
    | 'fieldNames'
    | 'multiple'
    | 'treeData'
    | 'virtual'
    | 'checkable'
    | 'selectable'
    | 'checkedKeys'
    | 'selectedKeys'
    | 'onSelect'
    | 'autoExpandParent'
    | 'expandedKeys'
    | 'height'
  > {
  envType?: 'test' | 'prod';
  jsonFrom?: OrgTreeJsonFromEnum;
  jsonType?: OrgTreeJsonTypeEnum;
  hierarchyList?: string[];
  selectHierarchy?: string;

  treeData?: LegoTreeTypes.ITreeNodesType[];
  loading?: boolean;
  dataListMap?: Record<string, LegoTreeTypes.ITreeNodesType>;
  request?: () => Promise<LegoTreeTypes.ITreeNodesType[]>;

  value: string[] | LegoTreeTypes.ICheckedAreaItem[];
  placeholder?: string;
  disabled?: boolean;
  includeArea?: boolean;
  searchDomHeight?: number;
  customFormatName?: (
    value: (LegoTreeTypes.ICheckedAreaItem | string)[],
    type: 'input' | 'modal'
  ) => string | undefined;

  keepOriginNode?: boolean;
}
```

### API
| 参数 | 说明 | 类型 | 是否可选 | 默认值 |
| --- | --- | --- | --- | --- |
| envType | 环境（测试 \| 生产）。走组件默认接口请求时使用，其余场景无需传该字段 | `test \| prod` | 是 | `test` |
| jsonFrom | 组织机构树 json 来源。走组件默认接口请求时使用，其余场景无需传该字段 | `LegoTreeTypes.OrgTreeJsonFromEnum` | 是 | `LegoTreeTypes.OrgTreeJsonFromEnum.SSO` |
| jsonType | 组织机构树 json 类型：全量(包含已关停等) or 有效的。走组件默认接口请求时使用，其余场景无需传该字段 | `LegoTreeTypes.OrgTreeJsonTypeEnum` | 是 | `LegoTreeTypes.OrgTreeJsonTypeEnum.ENABLE` |
| hierarchyList | 组织机构级别数组，01-07，用于过滤不包含的节点信息，没传或为空则不过滤。走组件默认接口请求时使用，其余场景无需传该字段 | `string[]` | 是 | - |
| selectHierarchy | 选择的组织机构级别，01-07，用于过滤子节点不包含 `selectHierarchy` 层级的节点。走组件默认接口请求时使用，其余场景无需传该字段 | `string` | 是 | - |
| treeData | antd vue `TreeSelect` 组件数据源（已经经过转化后的数据源，若 `includeArea` 为 `true`，则子节点需要包含 `trace` 字段） | `LegoTreeTypes.ITreeNodesType[]` | 是 | - |
| loading | 外部数据源加载时的 `loading` 状态 | `boolean` | 是 | false |
| dataListMap | 扁平化 map（已经经过转化后的数据源生成的 map） | `Record<string, LegoTreeTypes.ITreeNodesType>` | 是 | {} |
| request | 自定义服务，用于获取组件树数据源 | `() => Promise<LegoTreeTypes.ITreeNodesType[]>` | 是 | - |
| value(v-model) | 当前选中值，区分选择父子节点和子节点，有 2 种类型 | `string[] \| LegoTreeTypes.ICheckedAreaItem[]` | 否 | [] |
| placeholder | `Input.Search` 输入框的 `placeholder` | `string` | 是 | '搜索关键词' |
| disabled | tree 选择是否禁用 | `boolean` | 是 | false |
| includeArea | **是否包含区域选择，即是否允许选择各层级父子节点** | `boolean` | 是 | false |
| searchDomHeight | 实际业务中搜索输入框的高度，不同的样式主题高度会有差别 | `number` | 是 | 46 |
| customFormatName | 自定义选中项内容展示，包含 `input` 输入框的回显和 `Tree` 选中后的汇总 | `(value: (LegoTreeTypes.ICheckedAreaItem \| string)[], type: 'input' \| 'modal') => string \| undefined` | 是 | - |
| keepOriginNode | 是否树节点保留原始对象信息。若开启，配合 `includeArea` 为 `true`，则会返回 `originNode`（原始对象信息） | `boolean` | 是 | `false` |
| title | 自定义标题，在原始基础上，增加当前搜索值 `searchValue` 字段，供自定义判断处理 | `slot({ key, title, ..., searchValue })` | 是 | - |

### 事件(emit)
| 事件名称 | 说明 | 回调参数 |
| --- | --- | --- |
| change | 选中/反选树节点时调用此函数 | `function(string[] \| LegoTreeTypes.ICheckedAreaItem[])` |
| checkBefore | `Tree` `onCheck` 触发时优先调用此函数，此函数第三个参数返回 组件内部的树数据源 | `function(checked: Key[] \| { checked: Key[]; halfChecked: Key[]; }, info: CheckInfo, treeInfo: { treeData: LegoTreeTypes.ITreeNodesType[]; dataListMap: Record<string, LegoTreeTypes.ITreeNodesType> })` |
| getTreeInfo | 接口请求完成后，调用此函数，返回当前组件内部的树数据源 | `function({ treeData: LegoTreeTypes.ITreeNodesType[]; dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>; })` |

### ref

通过 ref 获取组件提供的方法或属性。以下是提供的方法或属性：
| 方法/属性名称 | 说明 | 类型 |
| --- | --- | --- |
| getTreeInfo | 【方法】提供组件内部的树数据源 | `() => ({ treeData: LegoTreeTypes.ITreeNodesType[]; dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>; })` |

### 效果展示

测试环境地址：https://yf-test-oss.yifengx.com/webtest/yangwen/legoTree/index.html#/treeTransfer

### 使用示例

```vue
<template>
  <ConfigProvider :locale="zh_CN">
    <div :class="$style.wrap">
      <!-- 有效数据源 -->
      <h4
        style="margin: 30px 50px 0 0; word-break: break-word; max-height: 100px; overflow-y: auto"
      >
        LegoTreeTransfer demo：{{ JSON.stringify(state.storeCodes) }}
      </h4>
      <div style="width: 536px; margin: 30px 0 0">
        <LegoTreeTransfer
          :treeData="state.treeData"
          :loading="state.loading"
          :include-area="true"
          :keep-origin-node="true"
          v-model:value="state.storeCodes"
        />
      </div>
      <h4
        style="margin: 30px 50px 0 0; word-break: break-word; max-height: 100px; overflow-y: auto"
      >
        LegoTreeTransfer demo：{{ JSON.stringify(state.storeCodes1) }}
      </h4>
      <Button type="primary" @click="getTreeInfoWithRef" style="margin-top: 10px">
        点我获取tree数据源
      </Button>
      <div style="width: 536px; margin: 30px 0 0">
        <LegoTreeTransfer
          :include-area="false"
          v-model:value="state.storeCodes1"
          :custom-format-name="customFormatName"
          :request="customRequest"
          ref="treeRef"
          @get-tree-info="getTreeInfo"
          @check-before="onCheckBefore"
        />
      </div>

      <!-- 账号中心 && 全部数据源（包含已关停） -->
      <div style="font-weight: bold; color: red; margin-top: 10px; margin-bottom: 10px">
        以下数据源为账号中心全量数据源（包含已关停数据）：
      </div>
      <h4
        style="margin: 30px 50px 0 0; word-break: break-word; max-height: 100px; overflow-y: auto"
      >
        LegoTreeTransfer demo：{{ JSON.stringify(state.storeCodesAll) }}
      </h4>
      <div style="width: 536px; margin: 30px 0 0">
        <LegoTreeTransfer
          :treeData="state.treeDataAll"
          :loading="state.loadingAll"
          :include-area="true"
          :keep-origin-node="true"
          v-model:value="state.storeCodesAll"
        />
      </div>
      <h4
        style="margin: 30px 50px 0 0; word-break: break-word; max-height: 100px; overflow-y: auto"
      >
        LegoTreeTransfer demo：{{ JSON.stringify(state.storeCodes1All) }}
      </h4>
      <Button type="primary" @click="getTreeInfoWithRefAll" style="margin-top: 10px">
        点我获取tree数据源
      </Button>
      <div style="width: 536px; margin: 30px 0 0">
        <LegoTreeTransfer
          :json-from="LegoTreeTypes.OrgTreeJsonFromEnum.USER"
          :json-type="LegoTreeTypes.OrgTreeJsonTypeEnum.ALL"
          env-type="test"
          :hierarchyList="['01', '02', '03', '04', '06', '07']"
          selectHierarchy="07"
          :include-area="false"
          v-model:value="state.storeCodes1All"
          ref="treeRefAll"
          @get-tree-info="getTreeInfoAll"
          @check-before="onCheckBeforeAll"
        />
      </div>
    </div>
  </ConfigProvider>
</template>

<script setup lang="ts">
import zh_CN from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs';
import { ConfigProvider, Button } from 'ant-design-vue';
import { reactive, onMounted, ref } from 'vue';
import { CheckInfo } from 'ant-design-vue/es/vc-tree/props';
import { LegoTreeTransfer, LegoTreeUtils, LegoTreeTypes, LegoTreeServices } from '@pluve/lego-tree-vue';
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
import { originList } from '@/constant/channel';

const treeRef = ref();
const treeRefAll = ref();

const state = reactive<{
  treeData: LegoTreeTypes.ITreeNodesType[];
  treeDataAll: LegoTreeTypes.ITreeNodesType[];
  loading: boolean;
  loadingAll: boolean;
  storeCodes: LegoTreeTypes.ICheckedAreaItem[];
  storeCodesAll: LegoTreeTypes.ICheckedAreaItem[];
  storeCodes1: string[];
  storeCodes1All: string[];
}>({
  treeData: [],
  treeDataAll: [],
  loading: false,
  loadingAll: false,
  storeCodes: [],
  storeCodesAll: [],
  storeCodes1: [],
  storeCodes1All: [],
});

const loadOrgTreeNodeByLevel = async () => {
  state.loading = true;
  try {
    const result = await LegoTreeServices.getAllOrgTreeNodesByOss({
      hierarchyList: ['01', '02', '03', '04', '06', '07'],
      envType: 'test',
    });
    const newTreeData = LegoTreeUtils.recursionTreeData({
      list: result?.[0].sub || [],
      pid: undefined,
      keepOriginNode: true,
    });
    LegoTreeUtils.excludeNode(newTreeData, '07');
    state.treeData = newTreeData;
  } catch (error) {
    state.treeData = [];
  } finally {
    state.loading = false;
  }
};

const loadOrgTreeNodeByLevelAll = async () => {
  state.loadingAll = true;
  try {
    const result = await LegoTreeServices.getAllOrgTreeNodesByOss({
      hierarchyList: ['01', '02', '03', '04', '06', '07'],
      envType: 'test',
      jsonFrom: LegoTreeTypes.OrgTreeJsonFromEnum.USER,
      jsonType: LegoTreeTypes.OrgTreeJsonTypeEnum.ALL,
    });
    const newTreeData = LegoTreeUtils.recursionTreeData({
      list: result?.[0].children || [],
      pid: undefined,
      childrenKey: 'children',
      hierarchyKey: 'rank',
      keepOriginNode: true,
      includeDisabledData: true,
    });
    LegoTreeUtils.excludeNode(newTreeData, '07');
    state.treeDataAll = newTreeData;
  } catch (error) {
    state.treeDataAll = [];
  } finally {
    state.loadingAll = false;
  }
};

onMounted(() => {
  loadOrgTreeNodeByLevel();
  loadOrgTreeNodeByLevelAll();
});

const customRequest = async () => {
  try {
    const result: any[] = await new Promise((resolve) => {
      setTimeout(() => {
        resolve(originList);
      }, 800);
    });
    const convertedData = LegoTreeUtils.recursionTreeData({
      list: result ?? [],
      valKey: 'id',
      nameKey: 'text',
      childrenKey: 'children',
    });
    return convertedData;
  } catch (error) {
    return [];
  }
};

const customFormatName = (
  values: (LegoTreeTypes.ICheckedAreaItem | string)[],
  type: 'input' | 'modal'
) => {
  const checkedStr = (values as LegoTreeTypes.ICheckedAreaItem[])
    .map((item) => item.title)
    .join('，');

  if (type === 'input') {
    return checkedStr ? `${checkedStr}被选择` : undefined;
  }

  return checkedStr ? `已选：${checkedStr}` : '已选：0条数据';
};

// 接口请求成功后返回对应的树数据
const getTreeInfo = ({
  treeData,
  dataListMap,
}: {
  treeData: LegoTreeTypes.ITreeNodesType[];
  dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>;
}) => {
  console.log('getTreeInfo', { treeData, dataListMap });
};

// 接口请求成功后返回对应的树数据
const getTreeInfoAll = ({
  treeData,
  dataListMap,
}: {
  treeData: LegoTreeTypes.ITreeNodesType[];
  dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>;
}) => {
  console.log('getTreeInfoAll', { treeData, dataListMap });
};

const onCheckBefore = (
  _: any,
  e: CheckInfo,
  treeInfo: {
    treeData: LegoTreeTypes.ITreeNodesType[];
    dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>;
  }
) => {
  console.log('treeInfo', treeInfo);
};

const onCheckBeforeAll = (
  _: any,
  e: CheckInfo,
  treeInfo: {
    treeData: LegoTreeTypes.ITreeNodesType[];
    dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>;
  }
) => {
  console.log('treeInfoAll', treeInfo);
};

const getTreeInfoWithRef = () => {
  console.log('treeRef.value', treeRef.value?.getTreeInfo());
};

const getTreeInfoWithRefAll = () => {
  console.log('treeRefAll.value', treeRefAll.value?.getTreeInfo());
};
</script>

<style lang="less" module>
.wrap {
  position: relative;
  min-height: 52px;
  background-color: #fff;
  padding: 0 80px 40px;
  border-radius: 4px;
}
</style>

```

## LegoTreeModal

通常作为 选择组织机构树的弹窗使用，Modal 内部引用 `LegoTreeTransfer` 组件来实现。故部分属性可以参考 `LegoTreeTransfer` 组件。

```ts
export interface ILegoTreeModalProps {
  title?: string;
  visible?: boolean;
  value: string[] | LegoTreeTypes.ICheckedAreaItem[];
  okLoading?: boolean;
  onOk?: (value: string[] | LegoTreeTypes.ICheckedAreaItem[]) => void;
  onCancel?: () => void;
  disabled?: boolean;
  showStoreUploadBtn?: boolean;
  modalProps?: Omit<
    ModalProps,
    'onOk' | 'onCancel' | 'visible' | 'destroyOnClose' | 'closable' | 'keyboard' | 'maskClosable'
  >;
  transferProps?: Omit<ILegoTreeTransferProps, 'value' | 'disabled'>;
}
```

### API
| 参数 | 说明 | 类型 | 是否可选 | 默认值 |
| --- | --- | --- | --- | --- |
| title | Modal 弹窗的 title，优先级：title[slot] > modalProps.title > title[string] | `string \| slot` | 是 | 选择区域 |
| visible | 控制 Modal 弹窗的显隐 | `boolean` | 是 | false |
| value(v-model) | 当前选中值，区分选择父子节点和子节点，有 2 种类型 | `string[] \| LegoTreeTypes.ICheckedAreaItem[]` | 否 | [] |
| okLoading | 点击弹窗确定按钮的 loading | `boolean` | 是 | false |
| onOk | 点击弹窗确定按钮触发 | `(value: string[] \| LegoTreeTypes.ICheckedAreaItem[]) => void` | 是 | - |
| onCancel | 点击弹窗取消按钮触发 | `() => void` | 是 | - |
| disabled | `Modal` 弹窗 和 `tree` 选择是否禁用 | `boolean` | 是 | false |
| showStoreUploadBtn | 是否展示默认的导入门店按钮，导入任意节点，做全量覆盖处理。<br>**注意：**<br>1. 如果走全量数据源（包含已关停数据），则不能使用默认的导入功能；<br>2. 如果开启 includeArea，则不区分导入的数据之间是否存在父子层级依赖关系，一并去重导入；  | `boolean` | 是 | false |
| modalProps | antd vue `Modal` 弹窗组件的额外的属性 | `Omit<ModalProps, 'onOk' \| 'onCancel' \| 'visible' \| 'destroyOnClose' \| 'closable' \| 'keyboard' \| 'maskClosable'>` | 是 | - |
| transferProps | `LegoTreeTransfer` 组件的额外的属性 | `Omit<ILegoTreeTransferProps, 'value' \| 'disabled'>` | 是 | {} |
| footer | Modal 弹窗的 footer，优先级最高 | `slot` | 是 | - |
| renderFooterLeft | Modal 弹窗底部左侧的自定义区域 | `slot` | 是 | - |
| renderUpload | Modal 弹窗底部右侧的自定义导入功能，`disabled` 为 `false` 时展示，优先级高于默认的导入功能  | `slot` | 是 | - |
| bodyTop | Modal 弹窗 `LegoTreeTransfer` 组件上方的自定义区域 | `slot` | 是 | - |
| bodyBottom | Modal 弹窗 `LegoTreeTransfer` 组件下方的自定义区域 | `slot` | 是 | - |
| treeTitle | 用于传递给 `LegoTreeTransfer` 组件的 title 插槽(避免插槽名字冲突) | `slot` | 是 | - |

### 事件(emit)
| 事件名称 | 说明 | 回调参数 |
| --- | --- | --- |
| getTreeInfo | 接口请求完成后，调用此函数，返回当前组件内部的树数据源 | `function({ treeData: LegoTreeTypes.ITreeNodesType[]; dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>; })` |

### ref

通过 ref 获取组件提供的方法或属性。以下是提供的方法或属性：
| 方法/属性名称 | 说明 | 类型 |
| --- | --- | --- |
| getTreeInfo | 【方法】提供组件内部的树数据源 | `() => ({ treeData: LegoTreeTypes.ITreeNodesType[]; dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>; })` |

### 效果展示

测试环境地址：https://yf-test-oss.yifengx.com/webtest/yangwen/legoTree/index.html#/treeModal

### 使用示例

```vue
<template>
  <ConfigProvider :locale="zh_CN">
    <div :class="$style.wrap">
      <!-- 有效数据源 -->
      <h4
        style="margin: 30px 50px 0 0; word-break: break-word; max-height: 100px; overflow-y: auto"
      >
        LegoTreeModal demo：{{ JSON.stringify(state.storeCodes) }}
      </h4>
      <Button style="margin-top: 20px" type="primary" @click="showTreeModal">
        点我打开组织机构树弹窗
      </Button>
      <LegoTreeModal
        :visible="state.treeModalVisible"
        v-model:value="state.storeCodes"
        @cancel="
          () => {
            state.treeModalVisible = false;
          }
        "
        @ok="
          (value) => {
            state.treeModalVisible = false;
          }
        "
        :showStoreUploadBtn="true"
        :transfer-props="{
          hierarchyList: ['01', '02', '03', '04', '06', '07'],
          selectHierarchy: '07',
          includeArea: false,
          onCheck: onTreeCheck,
        }"
        ref="treeRef"
        @get-tree-info="getTreeInfo"
      >
        <template #title>我是 LegoTreeModal 弹窗组件的自定义 title</template>
        <template #bodyTop>我是 LegoTreeModal 弹窗组件的自定义 bodyTop</template>
        <template #bodyBottom>我是 LegoTreeModal 弹窗组件的自定义 bodyBottom</template>
        <template #renderFooterLeft>我是 LegoTreeModal 弹窗组件的自定义 renderFooterLeft</template>
      </LegoTreeModal>

      <!-- 账号中心 && 全部数据源（包含已关停） -->
      <div style="font-weight: bold; color: red; margin-top: 40px; margin-bottom: 0px">
        以下数据源为账号中心全量数据源（包含已关停数据）：
      </div>
      <h4
        style="margin: 30px 50px 0 0; word-break: break-word; max-height: 100px; overflow-y: auto"
      >
        LegoTreeModal demo：{{ JSON.stringify(state.storeCodesAll) }}
      </h4>
      <Button style="margin-top: 20px" type="primary" @click="showTreeModalAll">
        点我打开组织机构树弹窗
      </Button>
      <LegoTreeModal
        :visible="state.treeModalVisibleAll"
        v-model:value="state.storeCodesAll"
        @cancel="
          () => {
            state.treeModalVisibleAll = false;
          }
        "
        @ok="
          (value) => {
            state.treeModalVisibleAll = false;
          }
        "
        :showStoreUploadBtn="false"
        :transfer-props="{
          jsonFrom: LegoTreeTypes.OrgTreeJsonFromEnum.USER,
          jsonType: LegoTreeTypes.OrgTreeJsonTypeEnum.ALL,
          hierarchyList: ['01', '02', '03', '04', '06', '07'],
          selectHierarchy: '07',
          includeArea: false,
          onCheck: onTreeCheckAll,
        }"
        ref="treeRefAll"
        @get-tree-info="getTreeInfoAll"
      >
        <template #title>我是 LegoTreeModal 弹窗组件的自定义 title</template>
        <template #bodyTop>我是 LegoTreeModal 弹窗组件的自定义 bodyTop</template>
        <template #bodyBottom>我是 LegoTreeModal 弹窗组件的自定义 bodyBottom</template>
        <template #renderFooterLeft>我是 LegoTreeModal 弹窗组件的自定义 renderFooterLeft</template>
      </LegoTreeModal>
    </div>
  </ConfigProvider>
</template>

<script setup lang="ts">
import zh_CN from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs';
import { ConfigProvider, Button } from 'ant-design-vue';
import { reactive, ref } from 'vue';
import { LegoTreeModal, LegoTreeTypes, LegoTreeUtils, LegoTreeServices } from '@pluve/lego-tree-vue';
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');

const treeRef = ref();
const treeRefAll = ref();

const state = reactive<{
  storeCodes: string[];
  storeCodesAll: string[];
  treeModalVisible: boolean;
  treeModalVisibleAll: boolean;
}>({
  storeCodes: [],
  storeCodesAll: [],
  treeModalVisible: false,
  treeModalVisibleAll: false,
});

const showTreeModal = () => {
  state.treeModalVisible = true;
};

const showTreeModalAll = () => {
  state.treeModalVisibleAll = true;
};

// 接口请求成功后返回对应的树数据
const getTreeInfo = ({
  treeData,
  dataListMap,
}: {
  treeData: LegoTreeTypes.ITreeNodesType[];
  dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>;
}) => {
  console.log('getTreeInfo', { treeData, dataListMap });
};

// 接口请求成功后返回对应的树数据
const getTreeInfoAll = ({
  treeData,
  dataListMap,
}: {
  treeData: LegoTreeTypes.ITreeNodesType[];
  dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>;
}) => {
  console.log('getTreeInfoAll', { treeData, dataListMap });
};

const onTreeCheck = () => {
  console.log('treeRef.value', treeRef.value?.getTreeInfo());
};

const onTreeCheckAll = () => {
  console.log('treeRefAll.value', treeRefAll.value?.getTreeInfo());
};
</script>

<style lang="less" module>
.wrap {
  position: relative;
  min-height: 52px;
  background-color: #fff;
  padding: 20px 80px;
  border-radius: 4px;
}
</style>
```

## LegoTree

通常用于多选组织架构，与 `FormItem` 进行绑定，内部引用 `LegoTreeModal` 组件来实现。故部分属性可以参考 `LegoTreeModal` 组件。

```ts
export interface ILegoTreeProps
  extends Omit<
    ILegoTreeModalProps,
    'title' | 'visible' | 'value' | 'onOk' | 'onCancel' | 'disabled'
  > {
  title?: string;
  value: string[] | LegoTreeTypes.ICheckedAreaItem[];
  disabled?: boolean;
  placeholder?: string;
  limitSelectedNum?: number;
  onlyStoreCodeInDisabled?: boolean;
  showModalInDisabled?: boolean;
  customValidate?: (value: string[] | LegoTreeTypes.ICheckedAreaItem[]) => Promise<boolean>;
  customFormatName?: (
    value: (LegoTreeTypes.ICheckedAreaItem | string)[],
    type: 'input' | 'modal'
  ) => string | undefined;
}
```

### API
| 参数 | 说明 | 类型 | 是否可选 | 默认值 |
| --- | --- | --- | --- | --- |
| title | `Modal` 弹窗的 `title` | `string \| slot` | 是 | 选择区域 |
| value(v-model) | 当前选中值，区分选择父子节点和子节点，有 2 种类型 | `string[] \| LegoTreeTypes.ICheckedAreaItem[]` | 否 | [] |
| disabled | `Input`、`Modal` 弹窗 和 `tree` 选择是否禁用 | `boolean` | 是 | false |
| placeholder | `Input` 输入框的 `placeholder` | `string` | 是 | '请选择区域' |
| limitSelectedNum | 限制最大门店勾选数量，仅限 transferProps.includeArea 为 false 时 | `number` | 是 | - |
| onlyStoreCodeInDisabled | 禁用时，是否只展示门店编码，仅限输入框中展示用 | `boolean` | 是 | false |
| showModalInDisabled | 禁用时，是否可以展示弹窗 | `boolean` | 是 | true |
| customValidate | 点击弹窗确定按钮，触发自定义校验，优先级：customValidate > limitSelectedNum | `(value: string[] \| ICheckedAreaItem[]) => Promise<boolean>` | 是 | - |
| customFormatName | 自定义选中项内容展示，包含 input 输入框的回显和 Tree 选中后的汇总 | `(value: (LegoTreeTypes.ICheckedAreaItem \| string)[], type: 'input' \| 'modal') => string \| undefined` | 是 | - |
| default | 默认插槽，用于自定义展示形式，默认为 `Input` | `slot({ show })` | 是 | - |

### 事件(emit)
| 事件名称 | 说明 | 回调参数 |
| --- | --- | --- |
| change | 弹窗选中/反选树节点后，点击【确定】按钮时调用此函数 | `function(string[] \| LegoTreeTypes.ICheckedAreaItem[])` |

### 效果展示

测试环境地址：https://yf-test-oss.yifengx.com/webtest/yangwen/legoTree/index.html#/tree

### 使用示例

```vue
<template>
  <ConfigProvider :locale="zh_CN">
    <div :class="$style.wrap">
      <Form :colon="false" autocomplete="off">
        <Row>
          <!-- 有效数据源 -->
          <Col :span="12">
            <FormItem label="Tree自定义request">
              <LegoTree
                title="选择渠道"
                placeholder="请选择"
                v-model:value="modelRef.storeCodes"
                :showStoreUploadBtn="true"
                :transfer-props="{
                  request: customRequest,
                  includeArea: false,
                }"
                :custom-format-name="customFormatName"
                :disabled="true"
                :show-modal-in-disabled="true"
              ></LegoTree>
            </FormItem>
          </Col>
          <Col :span="12"></Col>
          <Col :span="12">
            <FormItem label="Tree使用默认json">
              <LegoTree
                title="选择门店"
                placeholder="选择门店"
                v-model:value="modelRef.channelList"
                :limit-selected-num="100"
                :showStoreUploadBtn="true"
                :transfer-props="{
                  jsonFrom: LegoTreeTypes.OrgTreeJsonFromEnum.USER,
                  jsonType: LegoTreeTypes.OrgTreeJsonTypeEnum.ENABLE,
                  hierarchyList: ['01', '02', '03', '04', '06', '07'],
                  selectHierarchy: '07',
                  includeArea: true,
                  keepOriginNode: true,
                  onCheckBefore,
                }"
                :custom-validate="customValidate"
                :disabled="false"
                :show-modal-in-disabled="true"
                @get-tree-info="getTreeInfo"
              >
                <template #treeTitle="{ key: val, title, label }">
                  <b v-if="val === '5024'" style="color: #08c">{{ title || label }}</b>
                  <template v-else>{{ title || label }}</template>
                </template>
              </LegoTree>
            </FormItem>
          </Col>
          <Col :span="12"></Col>

          <!-- 账号中心 && 全部数据源（包含已关停） -->
          <Col :span="24" style="font-weight: bold; color: red; margin-bottom: 24px">
            以下数据源为账号中心全量数据源（包含已关停数据）：
          </Col>
          <Col :span="12">
            <FormItem label="Tree自定义treeData">
              <LegoTree
                title="选择渠道"
                placeholder="请选择"
                v-model:value="modelRef.storeCodesAll"
                :limit-selected-num="100"
                :showStoreUploadBtn="false"
                :transfer-props="{
                  treeData: state.treeDataAll,
                  loading: state.loadingAll,
                  includeArea: false,
                  keepOriginNode: true,
                  onCheckBefore: onCheckBeforeAll,
                }"
                :custom-validate="customValidate"
                :disabled="false"
                @get-tree-info="getTreeInfoAll"
              >
                <template #treeTitle="{ key: val, title, label, storeCode }">
                  <b v-if="val === '10004575_5024'" style="color: #08c">{{ title || label }}</b>
                  <b v-else-if="storeCode === '5500'" style="color: #f90">{{ title || label }}</b>
                  <template v-else>{{ title || label }}</template>
                </template>
              </LegoTree>
            </FormItem>
          </Col>
          <Col :span="12"></Col>
          <Col :span="12">
            <FormItem label="Tree使用默认json">
              <LegoTree
                title="选择门店"
                placeholder="选择门店"
                v-model:value="modelRef.channelListAll"
                :showStoreUploadBtn="false"
                :transfer-props="{
                  jsonFrom: LegoTreeTypes.OrgTreeJsonFromEnum.USER,
                  jsonType: LegoTreeTypes.OrgTreeJsonTypeEnum.ALL,
                  hierarchyList: ['01', '02', '03', '04', '06', '07'],
                  selectHierarchy: '07',
                  includeArea: true,
                  keepOriginNode: true,
                }"
                :disabled="true"
                :show-modal-in-disabled="true"
                :only-store-code-in-disabled="true"
              ></LegoTree>
            </FormItem>
          </Col>
          <Col :span="12"></Col>

          <Col :span="6">
            <Button @click="onQuery" style="margin-left: 10px" type="primary">查询</Button>
          </Col>
        </Row>
      </Form>
    </div>
  </ConfigProvider>
</template>

<script setup lang="ts">
import zh_CN from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs';
import { ConfigProvider, Form, FormItem, Row, Col, Button } from 'ant-design-vue';
import { reactive, onMounted } from 'vue';
import { CheckInfo } from 'ant-design-vue/es/vc-tree/props';
import { LegoTree, LegoTreeUtils, LegoTreeTypes, LegoTreeServices } from '@pluve/lego-tree-vue';
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
import { originList } from '@/constant/channel';

// 查询条件 ref
const modelRef = reactive<{
  storeCodes: string[];
  storeCodesAll: string[];
  channelList: LegoTreeTypes.ICheckedAreaItem[];
  channelListAll: LegoTreeTypes.ICheckedAreaItem[];
}>({
  storeCodes: ['5024', '5032', '5979', 'E520'],
  storeCodesAll: [],
  channelList: [],
  channelListAll: [
    {
      key: '10014212_7971',
      code: '10014212',
      storeCode: '7971',
      hierarchy: '07',
      title: '7971-南昌怡园路店',
    },
  ],
});

const state = reactive<{
  treeDataAll: LegoTreeTypes.ITreeNodesType[];
  loadingAll: boolean;
}>({
  treeDataAll: [],
  loadingAll: false,
});

// 查询条件 form
const form = Form.useForm(modelRef, undefined);

const customValidate = async (e: string[] | LegoTreeTypes.ICheckedAreaItem[]) => {
  await new Promise((resolve) => {
    setTimeout(() => {
      console.log('1秒的自定义校验');
      resolve(true);
    }, 1000);
  });
  return true;
};

const customRequest = async () => {
  try {
    const result: any[] = await new Promise((resolve) => {
      setTimeout(() => {
        resolve(originList);
      }, 800);
    });
    const convertedData = LegoTreeUtils.recursionTreeData({
      list: result ?? [],
      valKey: 'id',
      nameKey: 'text',
      childrenKey: 'children',
    });
    return convertedData;
  } catch (error) {
    return [];
  }
};

const customFormatName = (
  values: (LegoTreeTypes.ICheckedAreaItem | string)[],
  type: 'input' | 'modal'
) => {
  if (type === 'input') {
    const checkedStr = (values as string[]).join('，');
    return checkedStr ? `${checkedStr}被选择` : undefined;
  }

  const checkedStr = (values as LegoTreeTypes.ICheckedAreaItem[])
    .map((item) => item.title)
    .join('，');
  return checkedStr ? `已选：${checkedStr}` : '已选：0条数据';
};

const loadOrgTreeNodeByLevelAll = async () => {
  state.loadingAll = true;
  try {
    const result = await LegoTreeServices.getAllOrgTreeNodesByOss({
      hierarchyList: ['01', '02', '03', '04', '06', '07'],
      envType: 'test',
      jsonFrom: LegoTreeTypes.OrgTreeJsonFromEnum.USER,
      jsonType: LegoTreeTypes.OrgTreeJsonTypeEnum.ALL,
    });
    const newTreeData = LegoTreeUtils.recursionTreeData({
      list: result?.[0].children || [],
      pid: undefined,
      childrenKey: 'children',
      hierarchyKey: 'rank',
      keepOriginNode: true,
      includeDisabledData: true,
    });
    LegoTreeUtils.excludeNode(newTreeData, '07');
    state.treeDataAll = newTreeData;
  } catch (error) {
    state.treeDataAll = [];
  } finally {
    state.loadingAll = false;
  }
};

onMounted(() => {
  loadOrgTreeNodeByLevelAll();
});

const onQuery = () => {
  console.log('查询条件：', modelRef);
};

// 接口请求成功后返回对应的树数据
const getTreeInfo = ({
  treeData,
  dataListMap,
}: {
  treeData: LegoTreeTypes.ITreeNodesType[];
  dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>;
}) => {
  console.log('getTreeInfo', { treeData, dataListMap });
};

// 接口请求成功后返回对应的树数据
const getTreeInfoAll = ({
  treeData,
  dataListMap,
}: {
  treeData: LegoTreeTypes.ITreeNodesType[];
  dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>;
}) => {
  console.log('getTreeInfoAll', { treeData, dataListMap });
};

const onCheckBefore = (
  _: any,
  e: CheckInfo,
  treeInfo: {
    treeData: LegoTreeTypes.ITreeNodesType[];
    dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>;
  }
) => {
  console.log('treeInfo', treeInfo);
};

const onCheckBeforeAll = (
  _: any,
  e: CheckInfo,
  treeInfo: {
    treeData: LegoTreeTypes.ITreeNodesType[];
    dataListMap: Record<string, LegoTreeTypes.ITreeNodesType>;
  }
) => {
  console.log('treeInfoAll', treeInfo);
};
</script>

<style lang="less" module>
.wrap {
  position: relative;
  min-height: 52px;
  background-color: #fff;
  margin: 20px 0 0 100px;
  border-radius: 4px;
}
</style>
```

## LegoTreeUtils

提供一系列 `Tree` 相关方法

### HIERARCHY_LIST
提供 hierarchy 的枚举数组，枚举值：`LegoTreeTypes.CommonHierarchyEnum`

#### 示例

```ts
import { LegoTreeUtils } from '@pluve/lego-tree-vue';

const aaa = LegoTreeUtils.HIERARCHY_LIST;
```

### excludeNode
过滤树底层没有 `primaryKey` 的节点

#### Params

>`excludeNode(list: LegoTreeTypes.ITreeNodesType[], primaryKey?: string)`

| 参数 | 说明 | 类型 | 是否可选 | 默认值 |
| --- | --- | --- | --- | --- |
| list | Tree/TreeSel