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(() => {
- })
- }
- // 选中的数据 的 label
- let 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>
|