|
|
@@ -0,0 +1,346 @@
|
|
|
+<template>
|
|
|
+ <div class="xc-select-v2_main" @mousedown="stopLosingFocus($event)" ref="xcSelectV2Main">
|
|
|
+ <el-input ref="xcInput" @input="inputChange" v-model="label"
|
|
|
+ @blur="loseFocus"
|
|
|
+ :placeholder="placeholder"
|
|
|
+ @keydown.up.stop.prevent="onKeyboardNavigate('up')"
|
|
|
+ @keydown.down.stop.prevent="onKeyboardNavigate('down')"
|
|
|
+ @keydown.enter.stop.prevent="onKeyboardSelect"
|
|
|
+ @keydown.esc.stop.prevent="handleEsc"
|
|
|
+ @click="isShow = !isShow" @focus="getFocus">
|
|
|
+ <template #suffix>
|
|
|
+ <el-icon class="finger"
|
|
|
+ @mouseenter="isClearable = true"
|
|
|
+ @mouseleave="isClearable = false">
|
|
|
+ <span v-if="isClearable && props.clearable && props.modelValue[props.value]">
|
|
|
+ <CircleClose @click="clear"/>
|
|
|
+ </span>
|
|
|
+ <span v-else>
|
|
|
+ <ArrowUp v-show="isShow" @mousedown="clickIcon($event,false)"/>
|
|
|
+ <ArrowDown v-show="!isShow" @mousedown="clickIcon($event,true)"/>
|
|
|
+ </span>
|
|
|
+ </el-icon>
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+ <transition name="el-zoom-in-top">
|
|
|
+ <div v-show="isShow" class="xc-select-v2" :style="inputStyle">
|
|
|
+ <VirtualizationSelect :data="props.data" @change="change"
|
|
|
+ :value="props.modelValue[props.value]"
|
|
|
+ v-model="selectedSubscript"
|
|
|
+ :local-search="whetherToSearchLocally"
|
|
|
+ ref="selectItem" :data-length="dataLenght">
|
|
|
+ <template #default="{item}">
|
|
|
+ {{ item.value }}
|
|
|
+ {{ item.label }}
|
|
|
+ </template>
|
|
|
+ </VirtualizationSelect>
|
|
|
+ </div>
|
|
|
+ </transition>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup name='XcSelectV2'>
|
|
|
+import {debounce} from "@/utils/debounce";
|
|
|
+import VirtualizationSelect from './VirtualizationSelect.vue'
|
|
|
+import sleep from "@/utils/sleep";
|
|
|
+import {nextTick} from "vue";
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ modelValue: {
|
|
|
+ type: Object,
|
|
|
+ },
|
|
|
+ value: {
|
|
|
+ type: String,
|
|
|
+ default: 'code'
|
|
|
+ },
|
|
|
+ label: {
|
|
|
+ type: String,
|
|
|
+ default: 'name'
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ type: Array,
|
|
|
+ default: []
|
|
|
+ },
|
|
|
+ clearable: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ },
|
|
|
+ remoteMethod: {
|
|
|
+ type: Function,
|
|
|
+ default: null
|
|
|
+ },
|
|
|
+ maxCache: {
|
|
|
+ type: Number,
|
|
|
+ default: 100,
|
|
|
+ },
|
|
|
+ id: {
|
|
|
+ type: String,
|
|
|
+ default: 'id'
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const emit = defineEmits(['change'])
|
|
|
+
|
|
|
+let label = $ref('')
|
|
|
+let isGetFocus = $ref(false)
|
|
|
+let tempMap = new Map()
|
|
|
+let selectItem = $ref(null)
|
|
|
+let xcInput = $ref(null)
|
|
|
+let top = $ref(22)
|
|
|
+let selectedSubscript = $ref(0)
|
|
|
+let dataLenght = $ref(0)
|
|
|
+let isClearable = $ref(false)
|
|
|
+let inputStyle = $ref({
|
|
|
+ top: 22,
|
|
|
+ width: 120
|
|
|
+})
|
|
|
+let xcSelectV2Main = $ref()
|
|
|
+let whetherToSearchLocally = $ref(!props.remoteMethod)
|
|
|
+
|
|
|
+let isShow = $ref(false)
|
|
|
+
|
|
|
+let placeholder = computed(() => {
|
|
|
+ if (!label) {
|
|
|
+ return props.modelValue[props.label] ? props.modelValue[props.label] : '请选择'
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const clickIcon = async (e, val) => {
|
|
|
+ e.preventDefault()
|
|
|
+ isShow = val
|
|
|
+ await sleep(100)
|
|
|
+}
|
|
|
+
|
|
|
+const inputChange = debounce(value => {
|
|
|
+ if (value) {
|
|
|
+ method(value);
|
|
|
+ }
|
|
|
+}, 1000)
|
|
|
+
|
|
|
+
|
|
|
+const onKeyboardNavigate = (val) => {
|
|
|
+ if (val === 'up') {
|
|
|
+ selectedSubscript - 1 < 0 ? selectedSubscript = dataLenght - 1 : selectedSubscript -= 1
|
|
|
+ } else if (val === 'down') {
|
|
|
+ selectedSubscript + 1 > dataLenght - 1 ? selectedSubscript = 0 : selectedSubscript += 1
|
|
|
+ }
|
|
|
+ selectItem.selectedDataSubscript(selectedSubscript)
|
|
|
+}
|
|
|
+
|
|
|
+const onKeyboardSelect = () => {
|
|
|
+ if (isShow) {
|
|
|
+ change(props.data[selectedSubscript]);
|
|
|
+ } else {
|
|
|
+ isShow = true
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+const handleEsc = () => {
|
|
|
+ isShow = false
|
|
|
+}
|
|
|
+
|
|
|
+const method = (val) => {
|
|
|
+ if (props.remoteMethod) {
|
|
|
+ props.remoteMethod(val)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const change = async (data) => {
|
|
|
+ if (data) {
|
|
|
+ tempMap[data.value] = data
|
|
|
+ props.modelValue[props.value] = data.value
|
|
|
+ props.modelValue[props.label] = data.label
|
|
|
+ label = data.label
|
|
|
+ emit('change', data)
|
|
|
+ await sleep(200)
|
|
|
+ isShow = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const clear = () => {
|
|
|
+ props.modelValue[props.value] = ''
|
|
|
+ props.modelValue[props.label] = ''
|
|
|
+}
|
|
|
+
|
|
|
+const stopLosingFocus = (e) => {
|
|
|
+ // 阻止失去焦点
|
|
|
+ e.preventDefault()
|
|
|
+ xcInput.focus()
|
|
|
+}
|
|
|
+
|
|
|
+const getFocus = async () => {
|
|
|
+ await sleep(100)
|
|
|
+ if (!isShow) {
|
|
|
+ isShow = true
|
|
|
+ }
|
|
|
+ label = ''
|
|
|
+ isGetFocus = true
|
|
|
+}
|
|
|
+
|
|
|
+const loseFocus = async () => {
|
|
|
+ label = props.modelValue[props.label]
|
|
|
+ await sleep(100)
|
|
|
+ isShow = false
|
|
|
+ isGetFocus = false
|
|
|
+}
|
|
|
+
|
|
|
+const focus = () => {
|
|
|
+ xcInput.focus()
|
|
|
+}
|
|
|
+
|
|
|
+const blur = () => {
|
|
|
+ xcInput.focus()
|
|
|
+}
|
|
|
+
|
|
|
+defineExpose({focus, blur})
|
|
|
+
|
|
|
+const selectItemResetData = async () => {
|
|
|
+ try {
|
|
|
+ await selectItem.resetData()
|
|
|
+ } catch (e) {
|
|
|
+ console.log(e)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const selectItemEnableScrollBar = async () => {
|
|
|
+ try {
|
|
|
+ await selectItem.enableScrollBar()
|
|
|
+ } catch (e) {
|
|
|
+ // console.log(e)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+const valueChange = async () => {
|
|
|
+ dataLenght = props.data.length
|
|
|
+ await nextTick()
|
|
|
+ await selectItemResetData()
|
|
|
+ await selectItemEnableScrollBar()
|
|
|
+ if (isGetFocus) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ let val = props.modelValue[props.value]
|
|
|
+ if (val) {
|
|
|
+ // 在 数据中寻找 找到里就没有必要在找了
|
|
|
+ for (let i = 0; i < dataLenght; i++) {
|
|
|
+ let data = props.data[i];
|
|
|
+ if (data.value === val) {
|
|
|
+ props.modelValue[props.value] = data.value
|
|
|
+ props.modelValue[props.label] = data.label
|
|
|
+ label = data.label
|
|
|
+ selectedSubscript = i
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (props.modelValue[props.value] && props.modelValue[props.label]) {
|
|
|
+ label = props.modelValue[props.label]
|
|
|
+ props.data.push({
|
|
|
+ value: props.modelValue[props.value],
|
|
|
+ label: props.modelValue[props.label]
|
|
|
+ })
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!props.modelValue[props.label]) {
|
|
|
+ label = props.modelValue[props.value]
|
|
|
+ }
|
|
|
+ await method(props.modelValue[props.value])
|
|
|
+ } else {
|
|
|
+ label = props.modelValue[props.value]
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => props.modelValue[props.value],
|
|
|
+ (newValue, oldValue) => {
|
|
|
+ valueChange()
|
|
|
+ },
|
|
|
+ {immediate: true, deep: true}
|
|
|
+)
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => props.data,
|
|
|
+ async () => {
|
|
|
+ valueChange()
|
|
|
+ },
|
|
|
+ {immediate: true, deep: true}
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => isShow,
|
|
|
+ () => {
|
|
|
+ if (isShow && dataLenght > 0) {
|
|
|
+ nextTick(() => {
|
|
|
+ if (!props.data[selectedSubscript].value === props.modelValue[props.value]) {
|
|
|
+ for (let i = 0; i < dataLenght; i++) {
|
|
|
+ let data = props.data[i];
|
|
|
+ if (data.value === props.data[selectedSubscript].value) {
|
|
|
+ selectedSubscript = i
|
|
|
+ try {
|
|
|
+ selectItem.selectedDataSubscript(i)
|
|
|
+ } catch (e) {
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ selectItem.selectedDataSubscript(selectedSubscript)
|
|
|
+ }
|
|
|
+ let docInput = xcInput.input
|
|
|
+ let inputTop = docInput.getBoundingClientRect().top
|
|
|
+ let inputLeft = docInput.getBoundingClientRect().left
|
|
|
+ let selectBox = selectItem.$el
|
|
|
+ const {innerWidth: windowWidth, innerHeight: windowHeight} = window;
|
|
|
+ selectBox.style.direction = 'rtl'
|
|
|
+ if (inputLeft + selectBox.clientWidth > innerWidth) {
|
|
|
+ inputStyle['right'] = xcSelectV2Main.clientWidth + 'px'
|
|
|
+ inputStyle['top'] = 0 - (selectBox.clientHeight / 2) + 'px'
|
|
|
+ selectBox.style.direction = 'ltr'
|
|
|
+ }
|
|
|
+ if (inputTop + 320 >= innerHeight) {
|
|
|
+ inputStyle['top'] = 0 - (selectBox.clientHeight) + 'px'
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ nextTick(() => {
|
|
|
+ let docInput = xcInput.input
|
|
|
+ inputStyle = {
|
|
|
+ top: docInput.scrollHeight + 5 + 'px',
|
|
|
+ minWidth: docInput.scrollWidth + 15 + 'px',
|
|
|
+ minHeight: '40px'
|
|
|
+ }
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss">
|
|
|
+.xc-select-v2_main {
|
|
|
+ position: relative;
|
|
|
+ display: inline-block;
|
|
|
+
|
|
|
+ .xc-select-v2 {
|
|
|
+ box-shadow: 0 0 12px rgba(0, 0, 0, 0.12);
|
|
|
+ border-radius: 5px;
|
|
|
+ display: flex;
|
|
|
+ position: absolute;
|
|
|
+ flex-direction: column;
|
|
|
+ z-index: 1000;
|
|
|
+ top: 28px;
|
|
|
+ background-color: white;
|
|
|
+ }
|
|
|
+
|
|
|
+ .finger:hover {
|
|
|
+ cursor: pointer; //悬浮时变手指
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+</style>
|