7021f2fd创建于 2025年5月14日历史提交
<template>
  <view class="TreeSelect">
    <view class="pickerArea" :class="{ clear: !!showText }" @click="handleClick">
      <wd-input
        :placeholder="`请选择${$attrs.label}`"
        v-bind="$attrs"
        readonly
        v-model="showText"
      ></wd-input>
      <view v-if="!!showText && !$attrs.disabled" class="u-iconfont u-icon-close" @click.stop="handleClear"></view>
    </view>
    <wd-popup position="bottom" v-model="popupShow">
      <view class="content">
        <view class="operation">
          <view class="cancel text-gray-5" @click.stop="cancel">取消</view>
          <view class="confrim" @click.stop="confirm">确定</view>
        </view>
        <scroll-view class="flex-1" scroll-y>
          <DaTree
            v-if="popupShow"
            :data="treeData"
            labelField="title"
            valueField="key"
            loadMode
            :showCheckbox="multiple"
            :showRadioIcon="false"
            :checkStrictly="true"
            :loadApi="asyncLoadTreeData"
            :defaultCheckedKeys="defaultCheckedKeys"
            @change="handleTreeChange"
          ></DaTree>
        </scroll-view>
      </view>
    </wd-popup>
  </view>
</template>

<script setup lang="ts">
import { ref, watch, useAttrs } from 'vue'
import { useToast, useMessage, useNotify, dayjs } from 'wot-design-uni'
import { http } from '@/utils/http'
import DaTree from '@/uni_modules/da-tree/index.vue'
import { isArray } from '@/utils/is'
defineOptions({
  name: 'TreeSelect',
  options: {
    styleIsolation: 'shared',
  },
})
const props = defineProps({
  modelValue: {
    type: [Array, String],
  },
  dict: {
    type: String,
    default: 'id',
  },
  pidValue: {
    type: String,
    default: '',
  },
  pidField: {
    type: String,
    default: 'pid',
  },
  hasChildField: {
    type: String,
    default: '',
  },
  condition: {
    type: String,
    default: '',
    required: false,
  },
  converIsLeafVal: {
    type: Number,
    default: 1,
  },
  // 是否支持多选
  multiple: {
    type: Boolean,
    default: true,
  },
  hiddenNodeKey: {
    type: String,
    default: '',
  },
  // url: {
  //   type: String,
  //   default: '',
  // },
  // params: {
  //   type: Object,
  //   default: () => ({}),
  // },
})
const emit = defineEmits(['change', 'update:modelValue'])
const toast = useToast()
const api = {
  loadTreeData: '/sys/dict/loadTreeData',
  view: '/sys/dict/loadDictItem/',
}
const showText = ref('')
const popupShow = ref(false)
const treeData = ref<any[]>([])
const treeValue = ref([])
const tableName = ref<any>('')
const text = ref<any>('')
const code = ref<any>('')
const attrs = useAttrs()
const defaultCheckedKeys: any = ref(props.multiple ? [] : '')

const handleClick = () => {
  if (!attrs.disabled) {
    popupShow.value = true
  }
}
const cancel = () => {
  popupShow.value = false
}
const confirm = () => {
  const titles = treeValue.value.map((item) => item.title)
  const keys = treeValue.value.map((item) => item.key).join(',')
  showText.value = titles.join(',')
  popupShow.value = false
  emit('update:modelValue', keys)
  emit('change', keys)
}
const handleTreeChange = (value, record) => {
  const { originItem, checkedStatus } = record
  const { key, title } = originItem
  if (checkedStatus) {
    // 选中
    if (props.multiple) {
      treeValue.value.push({ key, title })
    } else {
      treeValue.value = [{ key, title }]
    }
  } else {
    // 取消
    if (props.multiple) {
      const findIndex = treeValue.value.findIndex((item) => item.key == key)
      if (findIndex != -1) {
        treeValue.value.splice(findIndex, 1)
      }
    } else {
      treeValue.value = []
    }
  }
}
const transformField = (result) => {
  for (let i of result) {
    i.value = i.key
    if (i.leaf == false) {
      i.isLeaf = false
    } else if (i.leaf == true) {
      i.isLeaf = true
    }
  }
}
// 异步加载
const asyncLoadTreeData = ({ originItem }) => {
  return new Promise<void>((resolve) => {
    let param = {
      pid: originItem.key,
      pidField: props.pidField,
      hasChildField: props.hasChildField,
      converIsLeafVal: props.converIsLeafVal,
      condition: props.condition,
      tableName: unref(tableName),
      text: unref(text),
      code: unref(code),
    }
    http
      .get(api.loadTreeData, param)
      .then((res: any) => {
        if (res.success) {
          const { result } = res
          transformField(result)
          resolve(result)
        } else {
          resolve(null)
        }
      })
      .catch((err) => resolve(null))
  })
}
// 加载根节点
function loadRoot() {
  let param = {
    pid: props.pidValue,
    pidField: props.pidField,
    hasChildField: props.hasChildField,
    condition: props.condition,
    converIsLeafVal: props.converIsLeafVal,
    tableName: unref(tableName),
    text: unref(text),
    code: unref(code),
  }
  http
    .get(api.loadTreeData, param)
    .then((res: any) => {
      if (res.success) {
        const { result } = res
        if (result && result.length > 0) {
          transformField(result)
          handleHiddenNode(result)
          treeData.value = result
        }
      } else {
        toast.warning('自定义树组件根节点数据加载失败~')
      }
    })
    .catch((err) => {
      toast.warning('自定义树组件根节点数据加载失败~')
    })
}
// 翻译input内的值
function loadItemByCode() {
  let value = props.modelValue
  if(value){
    if (isArray(props.modelValue)) {
      // @ts-ignore
      value = value.join()
    }
    if (value === treeData.value.map((item) => item.key).join(',')) {
      // 说明是刚选完,内部已有翻译。不需要再请求
      return
    }
    value = (value as string).trim()
    if (value) {
      http
        .get(`${api.view}${props.dict}`, { key: value })
        .then((res: any) => {
          if (res.success) {
            const { result = [] } = res
            showText.value = result.join(',')
            if (props.multiple) {
              defaultCheckedKeys.value = typeof value === 'string' ? value.split(',') : [value]
            } else {
              defaultCheckedKeys.value = value
            }
          } else {
          }
        })
        .catch((err) => {})
    }
  }
}
const initDictInfo = () => {
  let arr = props.dict?.split(',')
  tableName.value = arr[0]
  text.value = arr[1]
  code.value = arr[2]
}
const handleHiddenNode = (data) => {
  if (props.hiddenNodeKey && data?.length) {
    for (let i = 0, len = data.length; i < len; i++) {
      const item = data[i]
      if (item.key == props.hiddenNodeKey) {
        data.splice(i, 1)
        i--
        len--
        return
      }
    }
  }
}
const validateProp = () => {
  let mycondition = props.condition
  return new Promise<void>((resolve, reject) => {
    if (!mycondition) {
      resolve()
    } else {
      try {
        let test = JSON.parse(mycondition)
        if (typeof test == 'object' && test) {
          resolve()
        } else {
          toast.error('组件TreeSelect-condition传值有误,需要一个json字符串!')
          reject()
        }
      } catch (e) {
        toast.error('组件TreeSelect-condition传值有误,需要一个json字符串!')
        reject()
      }
    }
  })
}

// 清空
const handleClear = () => {
  showText.value = ''
  treeValue.value = []
  confirm()
}

watch(
  () => props.modelValue,
  () => loadItemByCode(),
  { deep: true, immediate: true },
)
watch(
  () => props.dict,
  () => {
    initDictInfo()
    loadRoot()
  },
)
watch(
  () => props.hiddenNodeKey,
  () => {
    if (treeData.value?.length && props.hiddenNodeKey) {
      handleHiddenNode(treeData.value)
      treeData.value = [...treeData.value]
    }
  },
)
// 初始化
validateProp().then(() => {
  initDictInfo()
  loadRoot()
  loadItemByCode()
})
</script>

<style lang="scss" scoped>
:deep(.wd-popup-wrapper) {
  .wd-popup {
    border-top-left-radius: 10px;
    border-top-right-radius: 10px;
  }
}
.content {
  height: 50vh;
  width: 100vw;
  display: flex;
  flex-direction: column;
  .operation {
    display: flex;
    justify-content: space-between;
    line-height: 40px;
    padding: 0 5px;
    position: relative;
    &::before {
      content: ' ';
      position: absolute;
      bottom: 0;
      left: 8px;
      right: 8px;
      height: 1px;
      background-color: #e5e5e5;
    }
    .cancel,
    .confrim {
      font-size: 15px;
      height: 40px;
      min-width: 40px;
      text-align: center;
    }
    .confrim {
      color: var(--wot-color-theme);
    }
  }
  :deep(.da-tree) {
    .da-tree-item__checkbox {
      // display: none;
    }
  }
}
.pickerArea {
  position: relative;
  .u-icon-close {
    position: absolute;
    right: 15px;
    top: calc(14px + 4px);
    color: #585858;
    font-size: 15px;
  }
  &.clear {
    :deep(.wd-input__body) {
      padding-right: 20px;
    }
  }
}
</style>