<template>
  <view class="custom-tree-select-content">
    <view
      :class="['select-list', { disabled }, { active: selectList.length }]"
      :style="boxStyle"
      @click.stop="open"
    >
      <view class="left">
        <view v-if="selectList.length" class="select-items">
          <view
            class="select-item"
            v-for="item in selectedListBaseinfo"
            :key="item[dataValue]"
          >
            <view class="name">
              <text>{{ pathMode ? item.path : item[dataLabel] }}</text>
            </view>
            <view
              v-if="!disabled && !item.disabled"
              class="close"
              @click.stop="removeSelectedItem(item)"
            >
              <uni-icons type="closeempty" size="16" color="#999"></uni-icons>
            </view>
          </view>
        </view>
        <view v-else style="color: #6a6a6a" class="no-data">
          <text style="#C0C4CB">{{ placeholder }}</text>
        </view>
      </view>
      <view class="right">
        <uni-icons
          v-if="!selectList.length || !clearable"
          type="bottom"
          size="14"
          color="#999"
        ></uni-icons>
        <uni-icons
          v-if="selectList.length && clearable"
          type="clear"
          size="24"
          color="#c0c4cc"
          @click.native.stop="clear"
        ></uni-icons>
      </view>
    </view>
    <uni-popup
      v-if="showPopup"
      ref="popup"
      :animation="animation"
      :is-mask-click="isMaskClick"
      :mask-background-color="maskBackgroundColor"
      :background-color="backgroundColor"
      :safe-area="safeArea"
      type="bottom"
      @change="change"
      @maskClick="maskClick"
    >
      <view
        class="popup-content"
        :style="{ height: contentHeight || defaultContentHeight }"
      >
        <view class="title">
          <view
            v-if="mutiple && canSelectAll"
            class="left"
            @click.stop="handleSelectAll"
          >
            <text>{{ isSelectedAll ? '取消全选' : '全选' }}</text>
          </view>
          <view class="center">
            <text>{{ placeholder }}</text>
          </view>
          <view
            class="right"
            :style="{ color: confirmTextColor }"
            @click.stop="close"
          >
            <text>{{ confirmText }}</text>
          </view>
        </view>
        <view v-if="search" class="search-box">
          <uni-easyinput
            :maxlength="-1"
            prefixIcon="search"
            placeholder="搜索"
            v-model="searchStr"
            confirm-type="search"
            @confirm="handleSearch(false)"
            @clear="handleSearch(true)"
          >
          </uni-easyinput>
          <button
            type="primary"
            size="mini"
            class="search-btn"
            @click.stop="handleSearch(false)"
          >
            搜索
          </button>
        </view>
        <view v-if="treeData.length" class="select-content">
          <scroll-view
            class="scroll-view-box"
            :scroll-top="scrollTop"
            scroll-y="true"
            @touchmove.stop
          >
            <view v-if="!filterTreeData.length" class="no-data center">
              <text>暂无数据</text>
            </view>
            <data-select-item
              v-for="item in filterTreeData"
              :key="item[dataValue]"
              :node="item"
              :dataLabel="dataLabel"
              :dataValue="dataValue"
              :dataChildren="dataChildren"
              :choseParent="choseParent"
              :border="border"
              :linkage="linkage"
              :load="load"
              :lazyLoadChildren="lazyLoadChildren"
            ></data-select-item>
            <view class="sentry" />
          </scroll-view>
        </view>
        <view v-else class="no-data center">
          <text>暂无数据</text>
        </view>
      </view>
    </uni-popup>
  </view>
</template>

<script>
const partCheckedSet = new Set()
import { paging } from './utils'
import dataSelectItem from './data-select-item.vue'
export default {
  name: 'custom-tree-select',
  components: {
    dataSelectItem
  },
  model: {
    prop: 'value',
    event: 'input'
  },
  props: {
    boxStyle: {
        type: String,
        default: ''
    },
    canSelectAll: {
      type: Boolean,
      default: false
    },
    safeArea: {
      type: Boolean,
      default: true
    },
    search: {
      type: Boolean,
      default: false
    },
    clearResetSearch: {
      type: Boolean,
      default: false
    },
    animation: {
      type: Boolean,
      default: true
    },
    'is-mask-click': {
      type: Boolean,
      default: true
    },
    'mask-background-color': {
      type: String,
      default: 'rgba(0,0,0,0.4)'
    },
    'background-color': {
      type: String,
      default: 'none'
    },
    'safe-area': {
      type: Boolean,
      default: true
    },
    choseParent: {
      type: Boolean,
      default: true
    },
    placeholder: {
      type: String,
      default: '请选择'
    },
    confirmText: {
      type: String,
      default: '完成'
    },
    confirmTextColor: {
      type: String,
      default: '#007aff'
    },
    contentHeight: {
      type: String
    },
    disabledList: {
      type: Array,
      default: () => []
    },
    listData: {
      type: Array,
      default: () => []
    },
    dataLabel: {
      type: String,
      default: 'name'
    },
    dataValue: {
      type: String,
      default: 'id'
    },
    dataChildren: {
      type: String,
      default: 'children'
    },
    linkage: {
      type: Boolean,
      default: false
    },
    clearable: {
      type: Boolean,
      default: false
    },
    mutiple: {
      type: Boolean,
      default: false
    },
    disabled: {
      type: Boolean,
      default: false
    },
    showChildren: {
      type: Boolean,
      default: false
    },
    border: {
      type: Boolean,
      default: false
    },
    pathMode: {
      type: Boolean,
      default: false
    },
    pathHyphen: {
      type: String,
      default: '/'
    },
    load: {
      type: Function,
      default: function () {}
    },
    lazyLoadChildren: {
      type: Boolean,
      default: false
    },
    value: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      defaultContentHeight: '500px',
      treeData: [],
      filterTreeData: [],
      clearTimerList: [],
      selectedListBaseinfo: [],
      showPopup: false,
      clickOpenTimer: null,
      isSelectedAll: false,
      scrollTop: 0,
      searchStr: ''
    }
  },
  computed: {
    selectList() {
      return this.value || []
    }
  },
  watch: {
    listData: {
      deep: true,
      immediate: true,
      handler(newVal) {
        if (newVal) {
          partCheckedSet.clear()
          this.treeData = this.initData(newVal)

          if (this.value) {
            this.changeStatus(this.treeData, this.value, true)
            this.filterTreeData.length &&
              this.changeStatus(this.filterTreeData, this.value)
          }
          if (this.showPopup) {
            this.resetClearTimerList()
            this.renderTree(this.treeData)
          }
        }
      }
    },
    value: {
      immediate: true,
      handler(newVal) {
        if (newVal) {
          this.changeStatus(this.treeData, this.value, true)
          this.filterTreeData.length &&
            this.changeStatus(this.filterTreeData, this.value)
        }
      }
    }
  },
  mounted() {
    this.getContentHeight(uni.getSystemInfoSync())
  },
  methods: {
    // 搜索完成返回顶部
    goTop() {
      this.scrollTop = 10
      this.$nextTick(() => {
        this.scrollTop = 0
      })
    },
    // 获取对应数据
    getReflectNode(node, arr) {
      const array = [...arr]
      while (array.length) {
        const item = array.shift()
        if (item[this.dataValue] === node[this.dataValue]) {
          return item
        }
        if (item[this.dataChildren]?.length) {
          array.push(...item[this.dataChildren])
        }
      }
      return {}
    },
    getContentHeight({ screenHeight }) {
      this.defaultContentHeight = `${Math.floor(screenHeight * 0.7)}px`
    },
    // 处理搜索
    handleSearch(isClear = false) {
      this.resetClearTimerList()
      if (isClear) {
        // 点击清空按钮并且设置清空按钮会重置搜索
        if (this.clearResetSearch) {
          this.renderTree(this.treeData)
        }
      } else {
        this.renderTree(this.searchValue(this.searchStr, this.treeData))
      }
      this.goTop()
      uni.hideKeyboard()
    },
    // 具体搜索方法
    searchValue(str, arr) {
      const res = []
      arr.forEach((item) => {
        if (item.visible) {
          if (
            item[this.dataLabel]
              .toString()
              .toLowerCase()
              .indexOf(str.toLowerCase()) > -1
          ) {
            res.push(item)
          } else {
            if (item[this.dataChildren]?.length) {
              const data = this.searchValue(str, item[this.dataChildren])
              if (data?.length) {
                if (
                  str &&
                  !item.showChildren &&
                  item[this.dataChildren]?.length
                ) {
                  item.showChildren = true
                }
                res.push({
                  ...item,
                  [this.dataChildren]: data
                })
              }
            }
          }
        }
      })
      return res
    },
    // 懒加载
    renderTree(arr) {
      const pagingArr = paging(arr)
      this.filterTreeData.splice(
        0,
        this.filterTreeData.length,
        ...(pagingArr?.[0] || [])
      )
      this.lazyRenderList(pagingArr, 1)
    },
    // 懒加载具体逻辑
    lazyRenderList(arr, startIndex) {
      for (let i = startIndex; i < arr.length; i++) {
        let timer = null
        timer = setTimeout(() => {
          this.filterTreeData.push(...arr[i])
        }, i * 500)
        this.clearTimerList.push(() => clearTimeout(timer))
      }
    },
    // 中断懒加载
    resetClearTimerList() {
      const list = [...this.clearTimerList]
      this.clearTimerList = []
      list.forEach((fn) => fn())
    },
    // 打开弹窗
    open() {
      // disaled模式下禁止打开弹窗
      if (this.disabled) return
      this.showPopup = true
      this.$nextTick(() => {
        this.$refs.popup.open()
        this.renderTree(this.treeData)
      })
    },
    // 关闭弹窗
    close() {
      this.$refs.popup.close()
    },
    // 弹窗状态变化 包括点击回显框和遮罩
    change(data) {
      if (data.show) {
        uni.$on('custom-tree-select-node-click', this.handleNodeClick)
        uni.$on('custom-tree-select-name-click', this.handleHideChildren)
        uni.$on('custom-tree-select-load', this.handleLoadNode)
      } else {
        uni.$off('custom-tree-select-node-click', this.handleNodeClick)
        uni.$off('custom-tree-select-name-click', this.handleHideChildren)
        uni.$off('custom-tree-select-load', this.handleLoadNode)
        this.resetClearTimerList()
        this.searchStr = ''
        if (this.animation) {
          setTimeout(() => {
            this.showPopup = false
          }, 200)
        } else {
          this.showPopup = false
        }
      }
      this.$emit('change', data)
    },
    // 点击遮罩
    maskClick() {
      this.$emit('maskClick')
    },
    // 初始化数据
    initData(arr, parentVisible = undefined, pathArr = []) {
      if (!Array.isArray(arr)) return []
      const res = []

      for (let i = 0; i < arr.length; i++) {
        const curPathArr = [...pathArr, arr[i][this.dataLabel]]
        const obj = {
          ...arr[i],
          [this.dataLabel]: arr[i][this.dataLabel],
          [this.dataValue]: arr[i][this.dataValue],
          path: curPathArr.join(this.pathHyphen)
        }

        obj.checked = this.selectList.includes(arr[i][this.dataValue])

        obj.disabled = false
        if (
          Boolean(arr[i].disabled) ||
          this.disabledList.includes(obj[this.dataValue])
        ) {
          obj.disabled = true
        }

        //半选
        obj.partChecked = Boolean(
          arr[i].partChecked === undefined ? false : arr[i].partChecked
        )
        obj.partChecked && obj.partCheckedSet.add(obj[this.dataValue])
        !obj.partChecked && (this.isSelectedAll = false)

        const parentVisibleState =
          parentVisible === undefined ? true : parentVisible
        const curVisibleState =
          arr[i].visible === undefined ? true : Boolean(arr[i].visible)
        if (parentVisibleState === curVisibleState) {
          obj.visible = parentVisibleState
        } else if (!parentVisibleState || !curVisibleState) {
          obj.visible = false
        } else {
          obj.visible = true
        }

        obj.showChildren =
          'showChildren' in arr[i] && arr[i].showChildren != undefined
            ? arr[i].showChildren
            : this.showChildren

        if (arr[i][this.dataChildren]?.length) {
          const childrenVal = this.initData(
            arr[i][this.dataChildren],
            obj.visible,
            curPathArr
          )
          obj[this.dataChildren] = childrenVal
          if (
            !obj.checked &&
            childrenVal.some((item) => item.checked || item.partChecked)
          ) {
            obj.partChecked = true
            partCheckedSet.add(obj[this.dataValue])
          }
        }

        res.push(obj)
      }

      return res
    },
    // 获取某个节点后面所有元素
    getChildren(node) {
      if (!node[this.dataChildren]?.length) return []
      const res = node[this.dataChildren].reduce((pre, val) => {
        if (val.visible) {
          return [...pre, val]
        }
        return pre
      }, [])
      for (let i = 0; i < node[this.dataChildren].length; i++) {
        res.push(...this.getChildren(node[this.dataChildren][i]))
      }
      return res
    },
    // 获取某个节点所有祖先元素
    getParentNode(target, arr) {
      let res = []

      for (let i = 0; i < arr.length; i++) {
        if (arr[i][this.dataValue] === target[this.dataValue]) {
          return true
        }

        if (arr[i][this.dataChildren]?.length) {
          const childRes = this.getParentNode(target, arr[i][this.dataChildren])
          if (typeof childRes === 'boolean' && childRes) {
            res = [arr[i]]
          } else if (Array.isArray(childRes) && childRes.length) {
            res = [...childRes, arr[i]]
          }
        }
      }

      return res
    },
    // 点击checkbox
    handleNodeClick(data, status) {
      const node = this.getReflectNode(data, this.treeData)
      node.checked = typeof status === 'boolean' ? status : !node.checked
      node.partChecked = false
      partCheckedSet.delete(node[this.dataValue])
      // 如果是单选不考虑其他情况
      if (!this.mutiple) {
        let emitData = []
        if (node.checked) {
          emitData = [node[this.dataValue]]
        }
        this.$emit('input', emitData)
      } else {
        // 多选情况
        if (!this.linkage) {
          // 不需要联动
          let emitData = null
          if (node.checked) {
            emitData = Array.from(
              new Set([...this.selectList, node[this.dataValue]])
            )
          } else {
            emitData = this.selectList.filter(
              (id) => id !== node[this.dataValue]
            )
          }
          this.$emit('input', emitData)
        } else {
          // 需要联动
          let emitData = [...this.selectList]
          const parentNodes = this.getParentNode(node, this.treeData)
          const childrenVal = this.getChildren(node).filter(
            (item) => !item.disabled
          )
          if (node.checked) {
            // 选中
            emitData = Array.from(new Set([...emitData, node[this.dataValue]]))
            if (childrenVal.length) {
              emitData = Array.from(
                new Set([
                  ...emitData,
                  ...childrenVal.map((item) => item[this.dataValue])
                ])
              )
              // 孩子节点全部选中并且清除半选状态
              childrenVal.forEach((childNode) => {
                childNode.partChecked = false
                partCheckedSet.delete(childNode[this.dataValue])
              })
            }
            if (parentNodes.length) {
              let flag = false
              // 有父元素 如果父元素下所有子元素全部选中，选中父元素
              while (parentNodes.length) {
                const item = parentNodes.shift()
                if (!item.disabled) {
                  if (flag) {
                    // 前一个没选中并且为半选那么之后的全为半选
                    item.partChecked = true
                    partCheckedSet.add(item[this.dataValue])
                  } else {
                    const allChecked = item[this.dataChildren]
                      .filter((node) => node.visible && !node.disabled)
                      .every((node) => node.checked)
                    if (allChecked) {
                      item.checked = true
                      item.partChecked = false
                      partCheckedSet.delete(item[this.dataValue])
                      emitData = Array.from(
                        new Set([...emitData, item[this.dataValue]])
                      )
                    } else {
                      item.partChecked = true
                      partCheckedSet.add(item[this.dataValue])
                      flag = true
                    }
                  }
                }
              }
            }
          } else {
            // 取消选中
            emitData = emitData.filter((id) => id !== node[this.dataValue])
            if (childrenVal.length) {
              // 取消选中全部子节点
              childrenVal.forEach((childNode) => {
                emitData = emitData.filter(
                  (id) => id !== childNode[this.dataValue]
                )
              })
            }
            if (parentNodes.length) {
              parentNodes.forEach((parentNode) => {
                if (emitData.includes(parentNode[this.dataValue])) {
                  parentNode.checked = false
                }
                emitData = emitData.filter(
                  (id) => id !== parentNode[this.dataValue]
                )
                const hasChecked = parentNode[this.dataChildren]
                  .filter((node) => node.visible && !node.disabled)
                  .some((node) => node.checked || node.partChecked)

                parentNode.partChecked = hasChecked
                if (hasChecked) {
                  partCheckedSet.add(parentNode[this.dataValue])
                } else {
                  partCheckedSet.delete(parentNode[this.dataValue])
                }
              })
            }
          }
          this.$emit('input', emitData)
        }
      }
    },
    // 点击名称折叠或展开
    handleHideChildren(node) {
      const status = !node.showChildren
      this.getReflectNode(node, this.treeData).showChildren = status
      this.getReflectNode(node, this.filterTreeData).showChildren = status
    },
    // 根据 dataValue 找节点
    changeStatus(list, ids, needEmit = false) {
      const arr = [...list]
      let flag = true
      needEmit && (this.selectedListBaseinfo = [])

      while (arr.length) {
        const item = arr.shift()

        if (ids.includes(item[this.dataValue])) {
          this.$set(item, 'checked', true)
          needEmit && this.selectedListBaseinfo.push(item)
          // 数据被选中清除半选状态
          item.partChecked = false
          partCheckedSet.delete(item[this.dataValue])
        } else {
          this.$set(item, 'checked', false)
          if (item.visible && !item.disabled) {
            flag = false
          }
          if (partCheckedSet.has(item[this.dataValue])) {
            this.$set(item, 'partChecked', true)
          } else {
            this.$set(item, 'partChecked', false)
          }
        }

        if (item[this.dataChildren]?.length) {
          arr.push(...item[this.dataChildren])
        }
      }
      this.isSelectedAll = flag
      needEmit && this.$emit('selectChange', [...this.selectedListBaseinfo])
    },
    // 移除选项
    removeSelectedItem(node) {
      this.isSelectedAll = false
      if (this.linkage) {
        this.handleNodeClick(node, false)
        this.$emit('removeSelect', node)
      } else {
        const emitData = this.selectList.filter(
          (item) => item !== node[this.dataValue]
        )
        this.$emit('removeSelect', node)
        this.$emit('input', emitData)
      }
    },
    // 全部选中
    handleSelectAll() {
      this.isSelectedAll = !this.isSelectedAll
      if (this.isSelectedAll) {
        if (!this.mutiple) {
          uni.showToast({
            title: '单选模式下不能全选',
            icon: 'none',
            duration: 1000
          })
          return
        }
        let emitData = []
        this.treeData.forEach((item) => {
          if (item.visible || (item.disabled && item.checked)) {
            emitData = Array.from(new Set([...emitData, item[this.dataValue]]))
            if (item[this.dataChildren]?.length) {
              emitData = Array.from(
                new Set([
                  ...emitData,
                  ...this.getChildren(item)
                    .filter(
                      (item) =>
                        !item.disabled || (item.disabled && item.checked)
                    )
                    .map((item) => item[this.dataValue])
                ])
              )
            }
          }
        })
        this.$emit('input', emitData)
      } else {
        this.clear()
      }
    },
    // 清空选项
    clear() {
      if (this.disabled) return
      const emitData = []
      partCheckedSet.clear()
      this.selectedListBaseinfo.forEach((node) => {
        if (node.visible && node.checked && node.disabled) {
          emitData.push(node[this.dataValue])
        }
      })
      this.$emit('input', emitData)
    },
    // 异步加载节点
    handleLoadNode({ source, target }) {
      // #ifdef MP-WEIXIN
      const node = this.getReflectNode(source, this.treeData)
      this.$set(
        node,
        this.dataChildren,
        this.initData(
          target,
          source.visible,
          source.path.split(this.pathHyphen)
        )
      )
      this.$nextTick(() => {
        this.handleHideChildren(node)
      })
      // #endif

      // #ifndef MP-WEIXIN
      this.$set(
        source,
        this.dataChildren,
        this.initData(
          target,
          source.visible,
          source.path.split(this.pathHyphen)
        )
      )
      this.handleHideChildren(source)
      // #endif
    }
  }
}
</script>

<style lang="scss" scoped>
$primary-color: #007aff;
$col-sm: 4px;
$col-base: 8px;
$col-lg: 12px;
$row-sm: 5px;
$row-base: 10px;
$row-lg: 15px;
$radius-sm: 3px;
$radius-base: 6px;

.custom-tree-select-content {
  .select-list {
    padding-left: $row-base;
    min-height: 35px;
    border: 1px solid #e5e5e5;
    border-radius: $radius-sm;
    display: flex;
    justify-content: space-between;
    align-items: center;

    &.active {
      padding: calc(#{$col-sm} / 2) 0 calc(#{$col-sm} / 2) $row-base;
    }

    .left {
      flex: 1;

      .select-items {
        display: flex;
        flex-wrap: wrap;
      }

      .select-item {
        margin: $col-sm $row-base $col-sm 0;
        padding: $col-sm $row-sm;
        max-width: auto;
        height: auto;
        background-color: #eaeaea;
        border-radius: $radius-sm;
        color: #333;
        display: flex;
        align-items: center;

        .name {
          flex: 1;
          padding-right: $row-base;
          font-size: 14px;
        }

        .close {
          width: 18px;
          height: 18px;
          display: flex;
          justify-content: center;
          align-items: center;
          overflow: hidden;
        }
      }
    }

    .right {
      margin-right: $row-sm;
      display: flex;
      justify-content: flex-end;
      align-items: center;
    }

    &.disabled {
      background-color: #f5f7fa;

      .left {
        .select-item {
          .name {
            padding: 0;
          }
        }
      }
    }
  }

  .popup-content {
    flex: 1;
    background-color: #fff;
    border-top-left-radius: 20px;
    border-top-right-radius: 20px;
    display: flex;
    flex-direction: column;

    .title {
      padding: $col-base 3rem;
      border-bottom: 1px solid $uni-border-color;
      font-size: 14px;
      display: flex;
      justify-content: space-between;
      position: relative;

      .left {
        position: absolute;
        left: 10px;
      }

      .center {
        flex: 1;
        text-align: center;
      }

      .right {
        position: absolute;
        right: 10px;
      }
    }

    .search-box {
      margin: $col-base $row-base 0;
      background-color: #fff;
      display: flex;
      align-items: center;

      .search-btn {
        margin-left: $row-base;
        height: 35px;
        line-height: 35px;
      }
    }

    .select-content {
      margin: $col-base $row-base;
      flex: 1;
      overflow: hidden;
      position: relative;
    }

    .scroll-view-box {
      touch-action: none;
      flex: 1;
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
    }

    .sentry {
      height: 48px;
    }
  }

  .no-data {
    width: auto;
    color: #C0C4CB !important;
    font-size: 15px;
  }

  .no-data.center {
    text-align: center;
  }
}
</style>
