| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550 | <template>  <div class="xc-select-v3 小手指"       :class="isFocus ? 'is-focus' : ''"       ref="selectRef"       @keydown.enter.stop.prevent="onKeyboardSelect"       @mousemove="mouseOverDiv = true"       @keydown.up.stop.prevent="onKeyboardNavigate('up')"       @keydown.down.stop.prevent="onKeyboardNavigate('down')"       @mouseout="mouseOverDiv = false">    <div class="box" @click="boxClick">      <input type="text"             @focus="getfocus"             autocomplete="new-pwd"             @blur="blur"             :id="props.id"             @mousedown.stop             @click="setUpSearchPlaceholders"             :placeholder="placeholder"             v-model="inputData"             ref="selectInputRef"/>      <span>          <el-icon              :class="showOptions ? 'turn' : ''"              style="transition: transform 0.4s">            <CircleClose v-if="whetherToErase" @click="clear"/>            <ArrowDown v-else/>          </el-icon>      </span>    </div>    <transition name="el-zoom-in-top">      <div class="selected_item"           ref="containerRef"           :style="{minWidth : `${selectRef?.clientWidth}` + 'px'}"           v-show="showOptions">        <div v-show="props.data.length === 0" class="empty_data">          暂无数据        </div>        <div :style="{ height: emptyHeight + 'px' }" v-show="props.data.length > 0"/>        <div :style="{ transform: `translateY(${translateY})`}"             style="width: 100%;"             v-show="props.data.length > 0">          <table style="width: 100%;">            <thead>            <tr style="line-height: 33px">              <th v-for="item in tableHeader">                {{ item.label }}              </th>            </tr>            </thead>            <tbody>            <tr v-for="(item,index) in listData"                @click.prevent.stop="clickToSelect(item)"                @mousemove="mouseOverLi(index)"                @mouseout="mouseOutLi()"                @mousedown.prevent                :class="currentIndex === startIndex + index ? 'hover' : '' "                :style="item.code === props.modelValue[props.code] ? selectedStyle : '' ">              <td v-for="itemTd in tableHeader">                {{ item[itemTd.prop] }}              </td>            </tr>            </tbody>          </table>        </div>      </div>    </transition>  </div></template><script setup name='XcSelectV3'>import {onClickOutside} from '@vueuse/core'import {functionDebounce} from "@/utils/debounce";const props = defineProps({  modelValue: {    type: Object,  },  data: {    type: Array,    default: []  },  code: {    type: String,    default: 'code'  },  name: {    type: String,    default: 'name'  },  clearable: {    type: Boolean,    default: false  },  id: {    type: String  },  remoteMethod: {    type: Function,    default: (val) => {      console.log('搜索内容%s', val)    }  },})const emit = defineEmits(['change'])const judgingCodeIsEmpty = () => {  if (props.modelValue[props.code] === null) {    return null  }  let codeType = typeof props.modelValue[props.code]  switch (codeType) {    case "number":      return props.modelValue[props.code] === null;    case "string":      return !props.modelValue[props.code]  }}const findSpecifiedData = () => {  return new Promise((resolve, reject) => {    if (judgingCodeIsEmpty()) {      props.modelValue[props.name] = null      reject();      return;    }    dataLength = props.data.length    if (dataLength === 0) {      reject()    }    for (let i = 0; i < dataLength; i++) {      if (props.data[i][props.code] === props.modelValue[props.code]) {        resolve({item: props.data[i], index: i})        return      }    }    reject()  })}const selectRef = ref(null)let isFocus = $ref(false)onClickOutside(selectRef, () => {  closePopup()})const selectInputRef = ref(null)const boxClick = async () => {  await nextTick()  selectInputRef.value?.focus()  isFocus = true  showOptions = !showOptions  if (showOptions) {    ifVisibleArea()    await edgeDetectionJudgment()  }}let placeholder = $ref('请选择')const setUpSearchPlaceholders = () => {  placeholder = props.modelValue[props.name]  inputData = null}//边缘检测判断const edgeDetectionJudgment = async () => {  await nextTick()  let {top, left} = selectRef.value.getBoundingClientRect()  let {clientHeight: inputHeight, clientWidth: inputWidth} = selectRef.value  let {clientHeight, clientWidth} = containerRef.value  const {innerWidth: windowWidth, innerHeight: windowHeight} = window;  if (left + clientWidth > windowWidth) {    containerRef.value.style.right = '0px'  }}// 输入框焦点const getfocus = async () => {  await nextTick()  selectInputRef.value?.focus()  isFocus = true}const blur = () => {  isFocus = false  closePopup()}// 关闭选项窗口const closePopup = () => {  showOptions = false  inputData = props.modelValue[props.name]  currentIndex = -1  if (judgingCodeIsEmpty()) {    placeholder = '请选择'  }}// 是否可以删除let mouseOverDiv = $ref(false)const whetherToErase = computed(() => {  return props.clearable && mouseOverDiv && props.modelValue[props.code]})// 显示 下拉框let showOptions = $ref(false)// 检查数据是否在可视区域const ifVisibleArea = () => {  let value = listData.value.filter((item, index) => {    if (item.code === props.modelValue[props.code]) {      currentIndex = startIndex + index      return true    }  })  if (value.length > 0) return;  findSpecifiedData().then(async ({item, index}) => {    await nextTick()    currentIndex = index    containerRef.value.scrollTop = index * itemHeight  }).catch(() => {  })}// 选中的数据 的 labellet inputData = $ref('')// 键盘事件const onKeyboardSelect = () => {  if (currentIndex === -1) {    boxClick()  } else {    clickToSelect(props.data[currentIndex])  }}// 上下选中事件let currentIndex = $ref(-1)const onKeyboardNavigate = async (position) => {  if (!showOptions) return  if (position === 'up') {    currentIndex--    if (currentIndex < 0) {      currentIndex = dataLength - 1    }  } else {    currentIndex++    if (currentIndex === dataLength) {      currentIndex = 0    }  }  let hoverIndex = currentIndex - startIndex  if (hoverIndex > itemCount - 1) {    containerRef.value.scrollTop += 34  } else if (hoverIndex < 0) {    containerRef.value.scrollTop -= 34  }  if (currentIndex === 0) {    containerRef.value.scrollTop = 0  } else if (currentIndex === dataLength - 1) {    containerRef.value.scrollTop = containerRef.value.scrollHeight  }}// 鼠标移动到let mousePosition = -1;const mouseOverLi = (index) => {  if (index !== mousePosition) {    mousePosition = index    currentIndex = startIndex + index  }}const mouseOutLi = () => {  mousePosition = -1}// 清空const clear = () => {  props.modelValue[props.code] = null  props.modelValue[props.name] = null  inputData = null}// 每个选项的大小const itemHeight = 34// 数据的长度let dataLength = 0;// 空的div 大小let emptyHeight = $ref()// 开始位置let startIndex = $ref(0)// 一次显示多少个let itemCount = $ref(7)//let translateY = $ref(0)const dataFilling = functionDebounce(() => {  findSpecifiedData().then(({item, index}) => {    clickToSelect(item)  }).catch(() => {    if (!showOptions) {      if (props.modelValue[props.name]) {        inputData = props.modelValue[props.name]      } else {        inputData = props.modelValue[props.code]      }    }  })}, 50)watch(() => props.data, async () => {  startIndex = 0  translateY = 0  currentIndex = -1  dataLength = props.data.length  emptyHeight = itemHeight * (dataLength + 2)  if (dataLength <= itemCount) {    emptyHeight = 0  }  await nextTick()  enableScrollBar()  containerRef.value.scrollTop = 0  if (judgingCodeIsEmpty()) {    dataFilling()  }}, {deep: true, immediate: true})watch(() => props.modelValue[props.code], async () => {  await nextTick()  dataFilling()  if (judgingCodeIsEmpty()) {    placeholder = '请选择'    inputData = null  }}, {deep: true, immediate: true})const listData = computed(() => {  return props.data.slice(startIndex, startIndex + itemCount)})// 监听容器事件const containerRef = ref(null)const enableScrollBar = () => {  containerRef.value.addEventListener('scroll', e => {    const {scrollTop} = e.target    startIndex = Math.floor(scrollTop / itemHeight)    translateY = scrollTop + 'px'    edgeDetectionJudgment()  })}// 点击li 选中数据const clickToSelect = (val) => {  props.modelValue[props.code] = val.code  props.modelValue[props.name] = val.name  inputData = val[props.name]  closePopup()}//选中样式const selectedStyle = {  backgroundColor: '#409eff',  color: '#fff'}// 远程搜索const performASearch = functionDebounce(() => {  if (inputData) {    props.remoteMethod(inputData)  }}, 200)const focus = () => {  getfocus()}defineExpose({focus})let tableHeader = $ref([])onMounted(() => {  nextTick(() => {    selectInputRef.value.addEventListener('input', () => {      performASearch()      showOptions = true    })  })  if (!!useSlots().default) {    useSlots().default().forEach((item) => {      tableHeader.push(item.props)    })  } else {    tableHeader.push({label: '编码', prop: 'code'})    tableHeader.push({label: '名称', prop: 'name'})  }})</script><style scoped lang="scss">.is-focus {  box-shadow: 0 0 0 1px #409eff inset !important;}.empty_data {  display: flex;  width: 100%;  justify-content: center;  align-items: center;  background-color: white;  color: #000;}.xc-select-v3 {  box-shadow: 0 0 0 1px #a8abb2 inset;  display: inline-block;  position: relative;  vertical-align: middle;  line-height: 22px;  border-radius: 4px;  background-color: white;  .box {    padding: 0 5px;    display: flex;    vertical-align: middle;    span {      font-size: 16px;      align-items: center;      display: flex;    }    input {      width: 100%;      flex-grow: 1;      -webkit-appearance: none;      font-size: inherit;      height: var(--el-input-inner-height);      line-height: var(--el-input-inner-height);      padding: 0;      outline: none;      border: none;      background: none;      box-sizing: border-box;    }  }  .selected_item {    position: absolute;    overflow: auto;    display: flex;    //direction: rtl;    z-index: 9999;    top: 26px;    box-shadow: 0 0 12px rgba(0, 0, 0, .12);    background-color: white;    min-height: 34px;    max-height: 275px;    &::-webkit-scrollbar {      width: 5px;      height: 20px;    }    &::-webkit-scrollbar-corner {      background-color: transparent;    }    &::-webkit-scrollbar-track {      -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);      border-radius: 10px;      background: #ededed;    }    &::-webkit-scrollbar-thumb {      border-radius: 10px;      -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);      background: #535353;    }  }}.turn {  transform: rotate(-180deg)}table {  border-spacing: 0;  background-color: white;  th {    text-align: left;  }}.hover {  background-color: #909399;  color: white;}th, td {  border-bottom: 1px solid #000;  white-space: nowrap;  padding: 0 5px;}tr {  line-height: 33px;}</style>
 |