XcTableV3.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. <template>
  2. <div class="xc-select-v3 小手指"
  3. :class="isFocus ? 'is-focus' : ''"
  4. ref="selectRef"
  5. @keydown.enter.stop.prevent="onKeyboardSelect"
  6. @mousemove="mouseOverDiv = true"
  7. @keydown.up.stop.prevent="onKeyboardNavigate('up')"
  8. @keydown.down.stop.prevent="onKeyboardNavigate('down')"
  9. @mouseout="mouseOverDiv = false">
  10. <div class="box" @click="boxClick">
  11. <input type="text"
  12. @focus="getfocus"
  13. autocomplete="new-pwd"
  14. @blur="blur"
  15. :id="props.id"
  16. @mousedown.stop
  17. @click="setUpSearchPlaceholders"
  18. :placeholder="placeholder"
  19. v-model="inputData"
  20. ref="selectInputRef"/>
  21. <span>
  22. <el-icon
  23. :class="showOptions ? 'turn' : ''"
  24. style="transition: transform 0.4s">
  25. <CircleClose v-if="whetherToErase" @click="clear"/>
  26. <ArrowDown v-else/>
  27. </el-icon>
  28. </span>
  29. </div>
  30. <transition name="el-zoom-in-top">
  31. <div class="selected_item"
  32. ref="containerRef"
  33. :style="{minWidth : `${selectRef?.clientWidth}` + 'px'}"
  34. v-show="showOptions">
  35. <div v-show="props.data.length === 0" class="empty_data">
  36. 暂无数据
  37. </div>
  38. <div :style="{ height: emptyHeight + 'px' }" v-show="props.data.length > 0"/>
  39. <div :style="{ transform: `translateY(${translateY})`}"
  40. style="width: 100%;"
  41. v-show="props.data.length > 0">
  42. <table style="width: 100%;">
  43. <thead>
  44. <tr style="line-height: 33px">
  45. <th v-for="item in tableHeader">
  46. {{ item.label }}
  47. </th>
  48. </tr>
  49. </thead>
  50. <tbody>
  51. <tr v-for="(item,index) in listData"
  52. @click.prevent.stop="clickToSelect(item)"
  53. @mousemove="mouseOverLi(index)"
  54. @mouseout="mouseOutLi()"
  55. @mousedown.prevent
  56. :class="currentIndex === startIndex + index ? 'hover' : '' "
  57. :style="item.code === props.modelValue[props.code] ? selectedStyle : '' ">
  58. <td v-for="itemTd in tableHeader">
  59. {{ item[itemTd.prop] }}
  60. </td>
  61. </tr>
  62. </tbody>
  63. </table>
  64. </div>
  65. </div>
  66. </transition>
  67. </div>
  68. </template>
  69. <script setup name='XcSelectV3'>
  70. import {onClickOutside} from '@vueuse/core'
  71. import {functionDebounce} from "@/utils/debounce";
  72. const props = defineProps({
  73. modelValue: {
  74. type: Object,
  75. },
  76. data: {
  77. type: Array,
  78. default: []
  79. },
  80. code: {
  81. type: String,
  82. default: 'code'
  83. },
  84. name: {
  85. type: String,
  86. default: 'name'
  87. },
  88. clearable: {
  89. type: Boolean,
  90. default: false
  91. },
  92. id: {
  93. type: String
  94. },
  95. remoteMethod: {
  96. type: Function,
  97. default: (val) => {
  98. console.log('搜索内容%s', val)
  99. }
  100. },
  101. })
  102. const emit = defineEmits(['change'])
  103. const judgingCodeIsEmpty = () => {
  104. if (props.modelValue[props.code] === null) {
  105. return null
  106. }
  107. let codeType = typeof props.modelValue[props.code]
  108. switch (codeType) {
  109. case "number":
  110. return props.modelValue[props.code] === null;
  111. case "string":
  112. return !props.modelValue[props.code]
  113. }
  114. }
  115. const findSpecifiedData = () => {
  116. return new Promise((resolve, reject) => {
  117. if (judgingCodeIsEmpty()) {
  118. props.modelValue[props.name] = null
  119. reject();
  120. return;
  121. }
  122. dataLength = props.data.length
  123. if (dataLength === 0) {
  124. reject()
  125. }
  126. for (let i = 0; i < dataLength; i++) {
  127. if (props.data[i][props.code] === props.modelValue[props.code]) {
  128. resolve({item: props.data[i], index: i})
  129. return
  130. }
  131. }
  132. reject()
  133. })
  134. }
  135. const selectRef = ref(null)
  136. let isFocus = $ref(false)
  137. onClickOutside(selectRef, () => {
  138. closePopup()
  139. })
  140. const selectInputRef = ref(null)
  141. const boxClick = async () => {
  142. await nextTick()
  143. selectInputRef.value?.focus()
  144. isFocus = true
  145. showOptions = !showOptions
  146. if (showOptions) {
  147. ifVisibleArea()
  148. await edgeDetectionJudgment()
  149. }
  150. }
  151. let placeholder = $ref('请选择')
  152. const setUpSearchPlaceholders = () => {
  153. placeholder = props.modelValue[props.name]
  154. inputData = null
  155. }
  156. //边缘检测判断
  157. const edgeDetectionJudgment = async () => {
  158. await nextTick()
  159. let {top, left} = selectRef.value.getBoundingClientRect()
  160. let {clientHeight: inputHeight, clientWidth: inputWidth} = selectRef.value
  161. let {clientHeight, clientWidth} = containerRef.value
  162. const {innerWidth: windowWidth, innerHeight: windowHeight} = window;
  163. if (left + clientWidth > windowWidth) {
  164. containerRef.value.style.right = '0px'
  165. }
  166. }
  167. // 输入框焦点
  168. const getfocus = async () => {
  169. await nextTick()
  170. selectInputRef.value?.focus()
  171. isFocus = true
  172. }
  173. const blur = () => {
  174. isFocus = false
  175. closePopup()
  176. }
  177. // 关闭选项窗口
  178. const closePopup = () => {
  179. showOptions = false
  180. inputData = props.modelValue[props.name]
  181. currentIndex = -1
  182. if (judgingCodeIsEmpty()) {
  183. placeholder = '请选择'
  184. }
  185. }
  186. // 是否可以删除
  187. let mouseOverDiv = $ref(false)
  188. const whetherToErase = computed(() => {
  189. return props.clearable && mouseOverDiv && props.modelValue[props.code]
  190. })
  191. // 显示 下拉框
  192. let showOptions = $ref(false)
  193. // 检查数据是否在可视区域
  194. const ifVisibleArea = () => {
  195. let value = listData.value.filter((item, index) => {
  196. if (item.code === props.modelValue[props.code]) {
  197. currentIndex = startIndex + index
  198. return true
  199. }
  200. })
  201. if (value.length > 0) return;
  202. findSpecifiedData().then(async ({item, index}) => {
  203. await nextTick()
  204. currentIndex = index
  205. containerRef.value.scrollTop = index * itemHeight
  206. }).catch(() => {
  207. })
  208. }
  209. // 选中的数据 的 label
  210. let inputData = $ref('')
  211. // 键盘事件
  212. const onKeyboardSelect = () => {
  213. if (currentIndex === -1) {
  214. boxClick()
  215. } else {
  216. clickToSelect(props.data[currentIndex])
  217. }
  218. }
  219. // 上下选中事件
  220. let currentIndex = $ref(-1)
  221. const onKeyboardNavigate = async (position) => {
  222. if (!showOptions) return
  223. if (position === 'up') {
  224. currentIndex--
  225. if (currentIndex < 0) {
  226. currentIndex = dataLength - 1
  227. }
  228. } else {
  229. currentIndex++
  230. if (currentIndex === dataLength) {
  231. currentIndex = 0
  232. }
  233. }
  234. let hoverIndex = currentIndex - startIndex
  235. if (hoverIndex > itemCount - 1) {
  236. containerRef.value.scrollTop += 34
  237. } else if (hoverIndex < 0) {
  238. containerRef.value.scrollTop -= 34
  239. }
  240. if (currentIndex === 0) {
  241. containerRef.value.scrollTop = 0
  242. } else if (currentIndex === dataLength - 1) {
  243. containerRef.value.scrollTop = containerRef.value.scrollHeight
  244. }
  245. }
  246. // 鼠标移动到
  247. let mousePosition = -1;
  248. const mouseOverLi = (index) => {
  249. if (index !== mousePosition) {
  250. mousePosition = index
  251. currentIndex = startIndex + index
  252. }
  253. }
  254. const mouseOutLi = () => {
  255. mousePosition = -1
  256. }
  257. // 清空
  258. const clear = () => {
  259. props.modelValue[props.code] = null
  260. props.modelValue[props.name] = null
  261. inputData = null
  262. }
  263. // 每个选项的大小
  264. const itemHeight = 34
  265. // 数据的长度
  266. let dataLength = 0;
  267. // 空的div 大小
  268. let emptyHeight = $ref()
  269. // 开始位置
  270. let startIndex = $ref(0)
  271. // 一次显示多少个
  272. let itemCount = $ref(7)
  273. //
  274. let translateY = $ref(0)
  275. const dataFilling = functionDebounce(() => {
  276. findSpecifiedData().then(({item, index}) => {
  277. clickToSelect(item)
  278. }).catch(() => {
  279. if (!showOptions) {
  280. if (props.modelValue[props.name]) {
  281. inputData = props.modelValue[props.name]
  282. } else {
  283. inputData = props.modelValue[props.code]
  284. }
  285. }
  286. })
  287. }, 50)
  288. watch(() => props.data, async () => {
  289. startIndex = 0
  290. translateY = 0
  291. currentIndex = -1
  292. dataLength = props.data.length
  293. emptyHeight = itemHeight * (dataLength + 2)
  294. if (dataLength <= itemCount) {
  295. emptyHeight = 0
  296. }
  297. await nextTick()
  298. enableScrollBar()
  299. containerRef.value.scrollTop = 0
  300. if (judgingCodeIsEmpty()) {
  301. dataFilling()
  302. }
  303. }, {deep: true, immediate: true})
  304. watch(() => props.modelValue[props.code], async () => {
  305. await nextTick()
  306. dataFilling()
  307. if (judgingCodeIsEmpty()) {
  308. placeholder = '请选择'
  309. inputData = null
  310. }
  311. }, {deep: true, immediate: true})
  312. const listData = computed(() => {
  313. return props.data.slice(startIndex, startIndex + itemCount)
  314. })
  315. // 监听容器事件
  316. const containerRef = ref(null)
  317. const enableScrollBar = () => {
  318. containerRef.value.addEventListener('scroll', e => {
  319. const {scrollTop} = e.target
  320. startIndex = Math.floor(scrollTop / itemHeight)
  321. translateY = scrollTop + 'px'
  322. edgeDetectionJudgment()
  323. })
  324. }
  325. // 点击li 选中数据
  326. const clickToSelect = (val) => {
  327. props.modelValue[props.code] = val.code
  328. props.modelValue[props.name] = val.name
  329. inputData = val[props.name]
  330. closePopup()
  331. }
  332. //选中样式
  333. const selectedStyle = {
  334. backgroundColor: '#409eff',
  335. color: '#fff'
  336. }
  337. // 远程搜索
  338. const performASearch = functionDebounce(() => {
  339. if (inputData) {
  340. props.remoteMethod(inputData)
  341. }
  342. }, 200)
  343. const focus = () => {
  344. getfocus()
  345. }
  346. defineExpose({focus})
  347. let tableHeader = $ref([])
  348. onMounted(() => {
  349. nextTick(() => {
  350. selectInputRef.value.addEventListener('input', () => {
  351. performASearch()
  352. showOptions = true
  353. })
  354. })
  355. if (!!useSlots().default) {
  356. useSlots().default().forEach((item) => {
  357. tableHeader.push(item.props)
  358. })
  359. } else {
  360. tableHeader.push({label: '编码', prop: 'code'})
  361. tableHeader.push({label: '名称', prop: 'name'})
  362. }
  363. })
  364. </script>
  365. <style scoped lang="scss">
  366. .is-focus {
  367. box-shadow: 0 0 0 1px #409eff inset !important;
  368. }
  369. .empty_data {
  370. display: flex;
  371. width: 100%;
  372. justify-content: center;
  373. align-items: center;
  374. background-color: white;
  375. color: #000;
  376. }
  377. .xc-select-v3 {
  378. box-shadow: 0 0 0 1px #a8abb2 inset;
  379. display: inline-block;
  380. position: relative;
  381. vertical-align: middle;
  382. line-height: 22px;
  383. border-radius: 4px;
  384. background-color: white;
  385. .box {
  386. padding: 0 5px;
  387. display: flex;
  388. vertical-align: middle;
  389. span {
  390. font-size: 16px;
  391. align-items: center;
  392. display: flex;
  393. }
  394. input {
  395. width: 100%;
  396. flex-grow: 1;
  397. -webkit-appearance: none;
  398. font-size: inherit;
  399. height: var(--el-input-inner-height);
  400. line-height: var(--el-input-inner-height);
  401. padding: 0;
  402. outline: none;
  403. border: none;
  404. background: none;
  405. box-sizing: border-box;
  406. }
  407. }
  408. .selected_item {
  409. position: absolute;
  410. overflow: auto;
  411. display: flex;
  412. //direction: rtl;
  413. z-index: 9999;
  414. top: 26px;
  415. box-shadow: 0 0 12px rgba(0, 0, 0, .12);
  416. background-color: white;
  417. min-height: 34px;
  418. max-height: 275px;
  419. &::-webkit-scrollbar {
  420. width: 5px;
  421. height: 20px;
  422. }
  423. &::-webkit-scrollbar-corner {
  424. background-color: transparent;
  425. }
  426. &::-webkit-scrollbar-track {
  427. -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
  428. border-radius: 10px;
  429. background: #ededed;
  430. }
  431. &::-webkit-scrollbar-thumb {
  432. border-radius: 10px;
  433. -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
  434. background: #535353;
  435. }
  436. }
  437. }
  438. .turn {
  439. transform: rotate(-180deg)
  440. }
  441. table {
  442. border-spacing: 0;
  443. background-color: white;
  444. th {
  445. text-align: left;
  446. }
  447. }
  448. .hover {
  449. background-color: #909399;
  450. color: white;
  451. }
  452. th, td {
  453. border-bottom: 1px solid #000;
  454. white-space: nowrap;
  455. padding: 0 5px;
  456. }
  457. tr {
  458. line-height: 33px;
  459. }
  460. </style>