lx-music-desktop/src/renderer/components/material/SearchList.vue

425 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template lang="pug">
div(:class="$style.container" ref="dom_container" v-show="isShow")
transition(enter-active-class="animated-fast zoomIn" leave-active-class="animated zoomOut" @after-leave="handleAnimated")
div(:class="$style.search" v-show="visible")
div(:class="$style.form")
input.key-bind.ignore-esc(:placeholder="placeholder" v-model.trim="text" ref="dom_input"
@input="handleDelaySearch"
@keyup.enter="handleTemplistClick(selectIndex)"
@keyup.40.prevent="handleKeyDown"
@keyup.38.prevent="handleKeyUp"
@keyup.27.prevent="handleKeyEsc"
@contextmenu="handleContextMenu")
button(type="button" @click="handleHide")
slot
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 212.982 212.982' space='preserve')
use(xlink:href='#icon-delete')
div.scroll(v-if="resultList" :class="$style.list" :style="listStyle" ref="dom_scrollContainer")
ul(ref="dom_list")
li(v-for="(item, index) in resultList" :key="item.songmid" :class="selectIndex === index ? $style.select : null" @mouseenter="selectIndex = index" @click="handleTemplistClick(index)")
div(:class="$style.img")
div(:class="$style.text")
h3(:class="$style.text") {{item.name}} - {{item.singer}}
h3(v-if="item.albumName" :class="[$style.text, $style.albumName]") {{item.albumName}}
div(:class="$style.source") {{item.source}}
</template>
<script>
import { clipboardReadText, debounce, scrollTo } from '../../utils'
let canceleFn
// https://blog.csdn.net/xcxy2015/article/details/77164126#comments
const similar = (a, b) => {
if (!a || !b) return 0
if (a.length > b.length) { // 保证 a <= b
let t = b
b = a
a = t
}
let al = a.length
let bl = b.length
let mp = [] // 一个表
let i, j, ai, lt, tmp // ai字符串a的第i个字符。 lt左上角的值。 tmp暂存新的值。
for (i = 0; i <= bl; i++) mp[i] = i
for (i = 1; i <= al; i++) {
ai = a.charAt(i - 1)
lt = mp[0]
mp[0] = mp[0] + 1
for (j = 1; j <= bl; j++) {
tmp = Math.min(mp[j] + 1, mp[j - 1] + 1, lt + (ai == b.charAt(j - 1) ? 0 : 1))
lt = mp[j]
mp[j] = tmp
}
}
return 1 - (mp[bl] / bl)
}
const sortInsert = (arr, data) => {
let key = data.num
let left = 0
let right = arr.length - 1
while (left <= right) {
let middle = parseInt((left + right) / 2)
if (key == arr[middle]) {
left = middle
break
} else if (key < arr[middle].num) {
right = middle - 1
} else {
left = middle + 1
}
}
while (left > 0) {
if (arr[left - 1].num != key) break
left--
}
arr.splice(left, 0, data)
}
const handleSortList = (list, keyword) => {
let arr = []
for (const item of list) {
sortInsert(arr, {
num: similar(keyword, `${item.name} ${item.singer} ${item.albumName || ''}`),
data: item,
})
}
return arr.map(item => item.data).reverse()
}
export default {
props: {
placeholder: {
type: String,
default: 'Find for something...',
},
list: {
type: Array,
default() {
return []
},
},
visible: {
type: Boolean,
default: false,
},
},
data() {
return {
text: '',
selectIndex: -1,
listStyle: {
height: 0,
maxHeight: 0,
},
maxHeight: 0,
resultList: [],
isModDown: false,
isShow: false,
}
},
watch: {
resultList(n) {
if (this.selectIndex > -1) this.selectIndex = -1
this.$nextTick(() => {
this.listStyle.height = Math.min(this.$refs.dom_list.scrollHeight, this.maxHeight) + 'px'
})
},
list(n) {
if (!this.visible) return
this.handleDelaySearch()
},
visible(n) {
if (!n) return
this.isShow = true
this.init()
},
},
created() {
this.handleDelaySearch = debounce(() => {
this.handleSearch()
})
if (this.visible) this.isShow = true
},
mounted() {
this.init()
window.eventHub.$on('key_mod_down', this.handle_key_mod_down)
window.eventHub.$on('key_mod_up', this.handle_key_mod_up)
window.eventHub.$on('key_mod+f_down', this.handle_key_mod_f_down)
},
beforeDestroy() {
window.eventHub.$off('key_mod_down', this.handle_key_mod_down)
window.eventHub.$off('key_mod_up', this.handle_key_mod_up)
window.eventHub.$off('key_mod+f_down', this.handle_key_mod_f_down)
},
methods: {
init() {
if (!this.visible) return
this.handleSearch()
this.$nextTick(() => {
if (!this.listStyle.maxHeight) {
this.maxHeight = this.$refs.dom_container.offsetParent.clientHeight - this.$refs.dom_list.offsetTop - 70
this.listStyle.maxHeight = this.maxHeight + 'px'
}
this.$refs.dom_input.focus()
})
},
handleKeyEsc() {
if (this.text.length > 0) {
this.text = ''
this.resultList = []
} else {
this.handleHide()
}
},
handle_key_mod_down() {
this.isModDown = true
},
handle_key_mod_up() {
setTimeout(() => {
this.isModDown = false
}, 100)
},
handle_key_mod_f_down() {
if (this.visible) this.$refs.dom_input.focus()
},
handleAnimated() {
if (this.visible) return
this.isShow = false
},
handleTemplistClick(index) {
if (index < 0) return
this.sendEvent('listClick', {
index: this.list.indexOf(this.resultList[index]),
isPlay: this.isModDown,
})
},
handleHide() {
this.sendEvent('hide')
},
sendEvent(action, data) {
this.$emit('action', {
action,
data,
})
},
handleKeyDown() {
if (!this.resultList.length) return
this.selectIndex = this.selectIndex + 1 < this.resultList.length ? this.selectIndex + 1 : 0
this.handleScrollList()
},
handleKeyUp() {
if (!this.resultList.length) return
this.selectIndex = this.selectIndex - 1 < -1 ? this.resultList.length - 1 : this.selectIndex - 1
this.handleScrollList()
},
handleScrollList() {
if (this.selectIndex < 0) return
let dom = this.$refs.dom_list.children[this.selectIndex]
let offsetTop = dom.offsetTop
let scrollTop = this.$refs.dom_scrollContainer.scrollTop
if (offsetTop < scrollTop) {
if (canceleFn) canceleFn()
canceleFn = scrollTo(this.$refs.dom_scrollContainer, offsetTop, 200, () => canceleFn = null)
} else if (offsetTop + dom.clientHeight > this.$refs.dom_scrollContainer.clientHeight + scrollTop) {
if (canceleFn) canceleFn()
canceleFn = scrollTo(this.$refs.dom_scrollContainer, offsetTop + dom.clientHeight - this.$refs.dom_scrollContainer.clientHeight, 200, () => canceleFn = null)
}
},
handleContextMenu() {
let str = clipboardReadText()
str = str.replace(/\t|\r\n|\n|\r/g, ' ')
str = str.replace(/\s+/g, ' ')
let dom_input = this.$refs.dom_input
this.text = `${this.text.substring(0, dom_input.selectionStart)}${str}${this.text.substring(dom_input.selectionEnd, this.text.length)}`
},
handleSearch() {
if (!this.text.length) return this.resultList = []
let list = []
let rxp = new RegExp(this.text.split('').join('.*'), 'i')
for (const item of this.list) {
if (rxp.test(`${item.name}${item.singer}${item.albumName ? item.albumName : ''}`)) list.push(item)
}
this.resultList = handleSortList(list, this.text)
},
},
}
</script>
<style lang="less" module>
@import '../../assets/styles/layout.less';
.container {
position: absolute;
left: 50%;
transform: translateX(-50%);
top: 20px;
width: 45%;
height: @height-toolbar * 0.52;
}
.search {
position: absolute;
width: 100%;
border-radius: 4px;
transition: box-shadow .4s ease, background-color @transition-theme;
display: flex;
flex-flow: column nowrap;
background-color: @color-search-form-background;
box-shadow: 0 1px 2px rgba(0,0,0,0.07),
0 2px 4px rgba(0,0,0,0.07),
0 4px 8px rgba(0,0,0,0.07),
0 8px 16px rgba(0,0,0,0.07),
0 16px 32px rgba(0,0,0,0.07),
0 32px 64px rgba(0,0,0,0.07);
&.active {
.form {
input {
border-bottom-left-radius: 0;
}
button {
border-bottom-right-radius: 0;
}
}
}
.form {
display: flex;
height: @height-toolbar * 0.52;
position: relative;
input {
flex: auto;
// border: 1px solid;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
background-color: transparent;
// border-bottom: 2px solid @color-theme;
// border-color: @color-theme;
border: none;
outline: none;
// height: @height-toolbar * .7;
padding: 0 5px;
overflow: hidden;
font-size: 13.5px;
line-height: @height-toolbar * 0.52 + 5px;
&::placeholder {
color: @color-btn;
}
}
button {
flex: none;
border: none;
// background-color: @color-search-form-background;
background-color: transparent;
outline: none;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
cursor: pointer;
height: 100%;
padding: 6px 7px;
color: @color-btn;
transition: background-color .2s ease;
opacity: 0.8;
&:hover {
background-color: @color-theme-hover;
}
&:active {
background-color: @color-theme-active;
}
}
}
.list {
// background-color: @color-search-form-background;
font-size: 13px;
transition: .3s ease;
height: 0;
transition-property: height;
position: relative;
li {
position: relative;
cursor: pointer;
padding: 8px 5px;
transition: background-color .2s ease;
line-height: 1.3;
// overflow: hidden;
display: flex;
flex-flow: row nowrap;
&.select {
background-color: @color-search-list-hover;
}
border-radius: 4px;
// &:last-child {
// border-bottom-left-radius: 4px;
// border-bottom-right-radius: 4px;
// }
}
}
}
.img {
flex: none;
}
.text {
flex: auto;
.mixin-ellipsis-1;
}
.albumName {
font-size: 12px;
opacity: 0.6;
}
.source {
flex: none;
font-size: 12px;
opacity: 0.5;
padding: 0 5px;
display: flex;
align-items: center;
// transform: rotate(45deg);
// background-color:
}
each(@themes, {
:global(#container.@{value}) {
.search {
background-color: ~'@{color-@{value}-search-form-background}';
.form {
input {
&::placeholder {
color: ~'@{color-@{value}-btn}';
}
}
button {
color: ~'@{color-@{value}-btn}';
&:hover {
background-color: ~'@{color-@{value}-theme-hover}';
}
&:active {
background-color: ~'@{color-@{value}-theme-active}';
}
}
}
.list {
li {
&.select {
background-color: ~'@{color-@{value}-search-list-hover}';
}
}
}
}
}
})
</style>