music / src /components /search /SearchBox.vue
ahutchen's picture
feat(search): 优化音乐源选择功能
7468cf4
<template>
<div class="search-box">
<div class="search-wrapper">
<div class="search-input-container">
<i class="fas fa-search search-icon"></i>
<input
v-model="searchKeyword"
type="text"
placeholder="搜索音乐、歌手、专辑..."
class="search-input"
@keypress.enter="handleSearch"
>
<button
v-if="searchKeyword"
class="clear-btn"
@click="clearSearch"
>
<i class="fas fa-times"></i>
</button>
<button
class="history-btn"
@click="showSearchHistory"
title="搜索历史"
>
<i class="fas fa-history"></i>
</button>
</div>
<button
v-if="isMobile"
class="source-btn"
@click="showSourceSelector = true"
>
{{ currentSourceName }}
<i class="fas fa-chevron-down"></i>
</button>
<select
v-else
:value="searchStore.currentSource"
@change="handleSourceChange"
class="pc-source-select"
>
<option
v-for="source in sortedSources"
:key="source.code"
:value="source.code"
>
{{ source.name }}
</option>
</select>
</div>
<!-- 音乐源选择弹窗 -->
<SourceSelector
v-if="showSourceSelector"
@close="showSourceSelector = false"
@select="handleSourceSelect"
/>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useSearchStore } from '@/stores/search'
import { MUSIC_SOURCES } from '@/services/musicApi'
import SourceSelector from './SourceSelector.vue'
const emit = defineEmits(['search'])
const router = useRouter()
const searchStore = useSearchStore()
// 响应式数据
const searchKeyword = ref('')
const showSourceSelector = ref(false)
// 计算属性
const currentSourceName = computed(() => {
const source = MUSIC_SOURCES.find(s => s.code === searchStore.currentSource)
return source ? source.name : '网易云音乐'
})
const sortedSources = computed(() =>
MUSIC_SOURCES.sort((a, b) => a.priority - b.priority)
)
const isMobile = computed(() => {
return window.innerWidth <= 768
})
// 方法
const handleSearch = () => {
const keyword = searchKeyword.value.trim()
if (keyword) {
emit('search', keyword)
}
}
const clearSearch = () => {
searchKeyword.value = ''
}
const clearKeyword = () => {
searchKeyword.value = ''
}
const showSearchHistory = () => {
// 跳转到搜索历史页面
router.push('/search-history')
}
const handleSourceSelect = (sourceCode) => {
searchStore.setSource(sourceCode)
showSourceSelector.value = false
}
const handleSourceChange = (event) => {
searchStore.setSource(event.target.value)
}
// 监听搜索关键词变化
watch(() => searchStore.keyword, (newKeyword) => {
if (newKeyword && newKeyword !== searchKeyword.value) {
searchKeyword.value = newKeyword
}
})
// 暴露方法给父组件
defineExpose({
clearKeyword
})
</script>
<style scoped>
.search-box {
position: relative;
background: transparent;
}
.search-wrapper {
display: flex;
gap: 8px;
align-items: center;
}
.search-input-container {
flex: 1;
position: relative;
display: flex;
align-items: center;
background: var(--bg-card);
border: 1px solid var(--border-light);
border-radius: 25px;
padding: 0 16px;
transition: var(--transition-fast);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
min-width: 0; /* 添加:允许容器收缩 */
}
.search-input-container:focus-within {
border-color: var(--accent-red);
box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.3), 0 4px 12px rgba(0, 0, 0, 0.1);
}
.search-icon {
color: var(--text-secondary);
margin-right: 8px;
font-size: 16px;
}
.search-input {
flex: 1;
height: 44px;
background: transparent;
border: none;
outline: none;
color: var(--text-primary);
font-size: 16px;
min-width: 0; /* 添加:允许输入框收缩 */
}
.search-input::placeholder {
color: var(--text-disabled);
}
.clear-btn {
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
border: none;
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
transition: var(--transition-fast);
}
.clear-btn:hover {
background: rgba(255, 255, 255, 0.3);
}
.history-btn {
width: 32px;
height: 32px;
border-radius: 50%;
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: var(--transition-fast);
margin-left: 4px;
}
.history-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--accent-red);
}
.source-btn {
height: 44px;
padding: 0 16px;
background: var(--bg-card);
border: 1px solid var(--border-light);
border-radius: 22px;
color: var(--text-primary);
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
transition: var(--transition-fast);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
flex-shrink: 0;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
}
.pc-source-select {
height: 44px;
padding: 0 16px;
background: var(--bg-card);
border: 1px solid var(--border-light);
border-radius: 22px;
color: var(--text-primary);
font-size: 14px;
cursor: pointer;
outline: none;
transition: var(--transition-fast);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
flex-shrink: 0;
min-width: 120px;
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6,9 12,15 18,9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 16px;
padding-right: 40px;
}
.pc-source-select:hover {
border-color: var(--accent-red);
color: var(--accent-red);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
.pc-source-select:focus {
border-color: var(--accent-red);
box-shadow: 0 0 0 3px rgba(255, 107, 107, 0.1);
}
.source-btn:hover {
border-color: var(--accent-red);
color: var(--accent-red);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
.source-btn i {
font-size: 12px;
}
/* 响应式 */
@media (max-width: 375px) {
.search-wrapper {
gap: 6px;
}
.source-btn {
padding: 0 12px;
font-size: 13px;
max-width: 100px; /* 调整小屏幕下的最大宽度 */
}
.search-input-container {
border: 1px solid var(--border-light);
}
.source-btn {
border: 1px solid var(--border-light);
}
}
@media (min-width: 768px) {
/* PC端优化 */
.search-input-container {
min-width: 200px; /* PC端设置最小宽度 */
}
.source-btn {
max-width: 150px; /* PC端增加最大宽度 */
}
}
</style>