mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
350 lines
9.0 KiB
Vue
350 lines
9.0 KiB
Vue
<template>
|
|
<div class="launch-bar">
|
|
<div class="input-area">
|
|
<div class="launch-bar-form">
|
|
<input
|
|
ref="input"
|
|
v-model="keyword"
|
|
class="input"
|
|
type="text"
|
|
autocomplete="off"
|
|
:placeholder="recommended.word"
|
|
@keydown.enter.stop="handleEnter"
|
|
@keydown.up.stop="handleUp"
|
|
@keydown.down.stop="handleDown"
|
|
/>
|
|
<button class="submit" title="执行" tabindex="-1" @click="handleEnter">
|
|
<VIcon icon="right-arrow" :size="20"></VIcon>
|
|
</button>
|
|
</div>
|
|
<!-- <div class="input-active-bar"></div> -->
|
|
</div>
|
|
<div ref="list" class="launch-bar-suggest-list">
|
|
<div v-if="isHistory" class="launch-bar-history-list">
|
|
<div v-if="actions.length === 0" class="history-empty suggest-item disabled" tabindex="0">
|
|
暂无搜索历史
|
|
</div>
|
|
<ActionItem
|
|
v-for="(a, index) of actions"
|
|
:key="a.key"
|
|
:action="a"
|
|
@previous-item="previousItem($event, index)"
|
|
@next-item="nextItem($event, index)"
|
|
@delete-item="onDeleteItem($event, index)"
|
|
@action="
|
|
() => {
|
|
index === actions.length - 1 && onClearHistory()
|
|
onAction()
|
|
}
|
|
"
|
|
/>
|
|
</div>
|
|
<div v-if="!isHistory" class="launch-bar-action-list">
|
|
<VEmpty
|
|
v-if="actions.length === 0 && noActions"
|
|
tabindex="0"
|
|
class="suggest-item disabled"
|
|
></VEmpty>
|
|
<VLoading
|
|
v-if="actions.length === 0 && !noActions"
|
|
tabindex="0"
|
|
class="suggest-item disabled"
|
|
></VLoading>
|
|
<ActionItem
|
|
v-for="(a, index) of actions"
|
|
:key="a.key"
|
|
:action="a"
|
|
@previous-item="previousItem($event, index)"
|
|
@next-item="nextItem($event, index)"
|
|
@delete-item="onDeleteItem($event, index)"
|
|
@action="onAction"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script lang="ts">
|
|
import Fuse from 'fuse.js'
|
|
import type { Ref } from 'vue'
|
|
import { defineComponent, ref } from 'vue'
|
|
|
|
import { select } from '@/core/spin-query'
|
|
import { matchUrlPattern } from '@/core/utils'
|
|
import { registerAndGetData } from '@/plugins/data'
|
|
import { VEmpty, VIcon, VLoading } from '@/ui'
|
|
|
|
import ActionItem from './ActionItem.vue'
|
|
import { historyProvider } from './history-provider'
|
|
import type { LaunchBarAction, LaunchBarActionProvider } from './launch-bar-action'
|
|
import { LaunchBarActionProviders } from './launch-bar-action'
|
|
import { search, searchProvider } from './search-provider'
|
|
|
|
const [actionProviders] = registerAndGetData(LaunchBarActionProviders, [
|
|
searchProvider,
|
|
historyProvider,
|
|
]) as [LaunchBarActionProvider[]]
|
|
const generateKeys = (
|
|
provider: LaunchBarActionProvider,
|
|
actions: LaunchBarAction[],
|
|
): ({
|
|
key: string
|
|
provider: LaunchBarActionProvider
|
|
} & LaunchBarAction)[] =>
|
|
actions.map(a => {
|
|
const key = `${provider.name}.${a.name}`
|
|
return {
|
|
...a,
|
|
key,
|
|
provider,
|
|
}
|
|
})
|
|
async function getOnlineActions(this: InstanceType<typeof ThisComponent>) {
|
|
const onlineActions = (
|
|
await Promise.all(
|
|
actionProviders.map(async provider =>
|
|
generateKeys(provider, await provider.getActions(this.keyword)),
|
|
),
|
|
)
|
|
).flat()
|
|
if (this.isHistory) {
|
|
return
|
|
}
|
|
const fuse = new Fuse(onlineActions, {
|
|
keys: ['indexer', 'displayName', 'name', 'description', 'key'],
|
|
})
|
|
const fuseResult = fuse.search(this.keyword)
|
|
console.log(fuseResult)
|
|
this.actions = fuseResult.map(it => it.item).slice(0, 12)
|
|
this.noActions = this.actions.length === 0
|
|
}
|
|
async function getActions(this: InstanceType<typeof ThisComponent>) {
|
|
this.noActions = false
|
|
if (this.isHistory) {
|
|
this.actions = generateKeys(historyProvider, await historyProvider.getActions(this.keyword))
|
|
return
|
|
}
|
|
this.actions = []
|
|
this.getOnlineActions().then()
|
|
}
|
|
|
|
const [recommended] = registerAndGetData('launchBar.recommended', {
|
|
word: '搜索',
|
|
href: 'https://search.bilibili.com/',
|
|
})
|
|
const ThisComponent = defineComponent({
|
|
components: {
|
|
VIcon,
|
|
VLoading,
|
|
VEmpty,
|
|
ActionItem,
|
|
},
|
|
emits: ['close'],
|
|
setup: () => ({
|
|
input: ref(null) as Ref<HTMLInputElement | null>,
|
|
list: ref(null) as Ref<HTMLDivElement | null>,
|
|
}),
|
|
data() {
|
|
return {
|
|
recommended,
|
|
actions: [] as ({
|
|
key: string
|
|
provider: LaunchBarActionProvider
|
|
} & LaunchBarAction)[],
|
|
keyword: '',
|
|
noActions: false,
|
|
}
|
|
},
|
|
computed: {
|
|
isHistory(): boolean {
|
|
return this.keyword.length === 0
|
|
},
|
|
},
|
|
watch: {
|
|
keyword() {
|
|
this.getActions()
|
|
},
|
|
},
|
|
async mounted() {
|
|
this.getActions().then()
|
|
if (!matchUrlPattern(/^https?:\/\/search\.bilibili\.com/)) {
|
|
return
|
|
}
|
|
select('#search-keyword').then((input: HTMLInputElement) => {
|
|
if (!input) {
|
|
return
|
|
}
|
|
this.keyword = input.value
|
|
document.addEventListener('change', e => {
|
|
if (!(e.target instanceof HTMLInputElement)) {
|
|
return
|
|
}
|
|
if (e.target.id === 'search-keyword') {
|
|
this.keyword = e.target.value
|
|
}
|
|
})
|
|
})
|
|
},
|
|
methods: {
|
|
getOnlineActions: lodash.debounce(getOnlineActions, 200) as unknown as () => Promise<void>,
|
|
getActions,
|
|
handleSelect() {
|
|
this.$emit('close')
|
|
this.getActions()
|
|
},
|
|
async handleEnter(e: KeyboardEvent | MouseEvent) {
|
|
if ('isComposing' in e && e.isComposing) {
|
|
return
|
|
}
|
|
if (this.actions.length > 0 && !this.isHistory) {
|
|
const [first] = this.actions
|
|
if (first.explicitSelect === false) {
|
|
first.action()
|
|
return
|
|
}
|
|
}
|
|
if (this.keyword) {
|
|
search(this.keyword)
|
|
this.handleSelect()
|
|
return
|
|
}
|
|
window.open(this.recommended.href, '_blank')
|
|
this.handleSelect()
|
|
},
|
|
handleUp(e: KeyboardEvent) {
|
|
if (e.isComposing) {
|
|
return
|
|
}
|
|
;(this.list.querySelector('.suggest-item:last-child') as HTMLElement).focus()
|
|
e.preventDefault()
|
|
},
|
|
handleDown(e: KeyboardEvent) {
|
|
if (e.isComposing) {
|
|
return
|
|
}
|
|
;(this.list.querySelector('.suggest-item') as HTMLElement).focus()
|
|
e.preventDefault()
|
|
},
|
|
previousItem(e: KeyboardEvent | MouseEvent, index: number) {
|
|
if (index === 0) {
|
|
this.focus()
|
|
} else {
|
|
;((e.currentTarget as HTMLElement).previousElementSibling as HTMLElement).focus()
|
|
}
|
|
},
|
|
nextItem(e: KeyboardEvent, index: number) {
|
|
const lastItemIndex = this.actions.length - 1
|
|
if (index !== lastItemIndex) {
|
|
;((e.currentTarget as HTMLElement).nextElementSibling as HTMLElement).focus()
|
|
} else {
|
|
this.focus()
|
|
}
|
|
},
|
|
search,
|
|
onDeleteItem(e: KeyboardEvent | MouseEvent, index: number) {
|
|
this.previousItem(e, index)
|
|
this.getActions()
|
|
},
|
|
onClearHistory() {
|
|
this.focus()
|
|
this.getActions()
|
|
},
|
|
onAction() {
|
|
// this.focus()
|
|
this.handleSelect()
|
|
},
|
|
focus() {
|
|
this.input.focus()
|
|
},
|
|
},
|
|
})
|
|
export default ThisComponent
|
|
</script>
|
|
<style lang="scss">
|
|
@import 'common';
|
|
.launch-bar {
|
|
--color: black;
|
|
color: var(--color);
|
|
position: relative;
|
|
@include h-center();
|
|
.input-area {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1;
|
|
.recommended-target {
|
|
display: none;
|
|
}
|
|
.launch-bar-form {
|
|
flex: 1;
|
|
display: flex;
|
|
.input {
|
|
flex: 1;
|
|
padding: 8px;
|
|
background-color: transparent;
|
|
border: none;
|
|
outline: none !important;
|
|
color: inherit;
|
|
box-sizing: border-box;
|
|
width: 15vw;
|
|
font-size: inherit;
|
|
&::placeholder {
|
|
color: inherit !important;
|
|
opacity: 0.8;
|
|
}
|
|
}
|
|
.submit {
|
|
color: inherit;
|
|
padding: 4px;
|
|
background: transparent;
|
|
border: none;
|
|
outline: none !important;
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
.input-active-bar {
|
|
flex: 0 0 auto;
|
|
align-self: flex-start;
|
|
height: 2px;
|
|
width: 0;
|
|
border-radius: 1px;
|
|
background-color: var(--theme-color);
|
|
transition: 0.3s ease-in-out;
|
|
}
|
|
}
|
|
.launch-bar-suggest-list {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 50%;
|
|
opacity: 0;
|
|
width: 100%;
|
|
transform: translateX(-50%) translateY(-4px);
|
|
pointer-events: none;
|
|
// transition: 0.2s ease-out;
|
|
border: 1px solid #8882;
|
|
white-space: nowrap;
|
|
border-radius: 8px;
|
|
@include shadow();
|
|
color: black;
|
|
background-color: #fff;
|
|
body.dark & {
|
|
color: #eee;
|
|
background-color: #222;
|
|
}
|
|
.suggest-highlight {
|
|
color: var(--theme-color);
|
|
font-style: normal;
|
|
}
|
|
}
|
|
&:focus-within {
|
|
.input-active-bar {
|
|
width: 100%;
|
|
}
|
|
}
|
|
&:focus-within .launch-bar-suggest-list,
|
|
.launch-bar-suggest-list:focus-within {
|
|
opacity: 1;
|
|
transform: translateX(-50%);
|
|
pointer-events: initial;
|
|
}
|
|
}
|
|
</style>
|