基本完成单曲搜索

This commit is contained in:
zilong 2021-11-01 00:28:41 +08:00
parent 03542b80a7
commit 82e56c3d51
10 changed files with 925 additions and 8 deletions

View File

@ -13,6 +13,7 @@ import SongStatus from "./views/common/SongStatus.vue";
import SongProgress from "@/views/common/SongProgress.vue";
import PlayingList from "@/views/common/PlayingList.vue";
import ZPlayingList from "@/views/common/ZPlayingList.vue";
import Searching from "@/views/common/Searching.vue";
import SongDetail from "@/views/common/SongDetail.vue";
import pubsub from "pubsub-js";
import {
@ -47,6 +48,7 @@ store.commit("loadCaches");
const showPlaying = ref(false); //
const showSongDetail = ref(false); //
const showSearch = ref(false); //
watch(
()=> store.state.showSongDetail,
@ -78,6 +80,12 @@ const token = pubsub.subscribe("zp", (msg, data) => {
case "zp.toggleSongDetail":
showSongDetail.value = store.state.showSongDetail = !showSongDetail.value;
break;
case "zp.showSearch":
showSearch.value = true;
break;
case "zp.toggleSearch":
showSearch.value = !showSearch.value ;
break;
}
});
@ -91,6 +99,7 @@ onUnmounted(() => {
const wpEl = ref('')
const hideWins = (e) => {
// console.log(e.clientX > 0,e, wpEl.value.clientWidth);
//
if (showPlaying.value
&& e.clientX > 0
&& e.clientX < wpEl.value.clientWidth - 500
@ -99,6 +108,15 @@ const hideWins = (e) => {
) {
showPlaying.value = false;
}
//
if (showSearch.value
&& e.clientX > 0
&& e.clientX < wpEl.value.clientWidth - 360
&& e.clientY > 40
&& e.clientY < wpEl.value.clientHeight - 64
) {
showSearch.value = false;
}
};
</script>
@ -144,6 +162,11 @@ const hideWins = (e) => {
<ZPlayingList />
</div>
</div>
<div id="wpSearch" v-show="showSearch">
<div id="search">
<Searching />
</div>
</div>
<div id="footer">
<div id="songDetail"></div>
<div id="songProgress">
@ -208,6 +231,7 @@ body {
display: flex;
flex-direction: column;
height: 100vh;
user-select: none;
#top {
height: 40px;
display: flex;
@ -252,6 +276,22 @@ body {
}
}
#wpSearch{
#search{
position: absolute;
top: 40px;
right: 0;
bottom: 64px;
width: 360px;
background-color: #fff;
z-index: 2000;
border: #ddd solid 1px;
overflow-y: auto;
// padding: 8px;
}
}
#footer {
display: flex;
flex-direction: column;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"><path d="M221.09 64a157.09 157.09 0 1 0 157.09 157.09A157.1 157.1 0 0 0 221.09 64z" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"></path><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M338.29 338.29L448 448"></path></svg>

After

Width:  |  Height:  |  Size: 415 B

332
src/components/Songlist.vue Normal file
View File

@ -0,0 +1,332 @@
<script setup>
import {
ref,
reactive,
h,
watch,
toRaw,
onMounted,
onUnmounted,
nextTick,
} from "vue";
import { RouterLink, useRoute, useRouter } from "vue-router";
import { useStore } from "vuex";
import {
NButton,
NButtonGroup,
NSpace,
NIcon,
NDropdown,
NMenu,
NLayout,
NLayoutHeader,
NLayoutFooter,
NLayoutContent,
NLayoutSider,
NTag,
NDataTable,
useMessage,
} from "naive-ui";
import Play from "@/assets/svgs/Play_.svg";
import Pause from "@/assets/svgs/Pause.svg";
import svgDots from "@/assets/svgs/Dots.svg";
import dayjs from "dayjs";
import "dayjs/locale/zh-cn";
import duration from "dayjs/plugin/duration";
import pubsub from "pubsub-js";
import ArtistsSpan from "@/components/ArtistsSpan.vue";
import AlbumSpan from "@/components/AlbumSpan.vue";
const store = useStore();
const router = useRouter();
dayjs.extend(duration);
const props = defineProps({
songs: Array,
});
// const s = ref(props.songs)
// console.log(s.value.value);
</script>
<template>
<div class="wp-list" ref="wpTable" v-if="false">
<div
class="tr"
v-for="(p, idx) in songs"
:key="idx"
draggable="true"
@dragstart="dragstart(idx)"
@dragenter="dragenter($event, idx)"
@dragover="dragover($event, idx)"
@drop="drop($event, idx)"
@dblclick="pubsub.publish('zp.play', { id: p.id, im: true })"
>
<div class="icon">
<NButton
v-show="p.id === store.state.settings.songId"
text
type="primary"
size="tiny"
>
<NIcon style="bottom: -1px; left: -2px; position: absolute">
<Play v-if="store.state.settings.playing"></Play>
<Pause v-else></Pause>
</NIcon>
</NButton>
</div>
<div class="name">
<span class="ntext">{{ p.name }}</span>
<span class="nm">
<n-dropdown
placement="right-start"
@select="handleSelect($event, p.id)"
trigger="click"
:show-arrow="true"
>
<NButton
class="mn"
text
type="primary"
style="font-size: 18px"
>
<NIcon>
<svg-dots />
</NIcon>
</NButton>
</n-dropdown>
</span>
</div>
<div class="ar">
<ArtistsSpan :artists="p.artists" />
</div>
<div class="al">{{ p.album.name }}</div>
<div class="dt">
{{ dayjs.duration(p.duration).format("mm:ss") }}
</div>
</div>
</div>
<table class="tbList">
<tr class="tr trh">
<td class="icon"></td>
<td class="name">音乐标题</td>
<td class="ar">歌手</td>
<td class="al">专辑</td>
<td class="dt">时长</td>
</tr>
<tr
class="tr"
v-for="p in songs"
:key="p.id"
draggable="false"
@dblclick="pubsub.publish('zp.play', { id: p.id, im: true })"
>
<td class="icon">
<NButton
v-show="p.id === store.state.settings.songId"
text
type="primary"
size="tiny"
>
<NIcon style="bottom: -1px; left: -2px; position: absolute">
<Play v-if="store.state.settings.playing"></Play>
<Pause v-else></Pause>
</NIcon>
</NButton>
</td>
<td class="name">
<span class="ntext">{{ p.name }}</span>
<span class="nm">
<n-dropdown
placement="right-start"
@select="handleSelect($event, p.id)"
trigger="click"
:show-arrow="true"
>
<NButton
class="mn"
text
type="primary"
style="font-size: 18px"
>
<NIcon>
<svg-dots />
</NIcon>
</NButton>
</n-dropdown>
</span>
</td>
<td class="ar"><ArtistsSpan :artists="p.artists" /></td>
<td class="al"><AlbumSpan :album="p.album" /></td>
<td class="dt">
{{ dayjs.duration(p.duration).format("mm:ss") }}
</td>
</tr>
</table>
</template>
<script>
export default {
name: "Songlist",
};
</script>
<style lang="less" scoped>
@import "@/assets/css/common.less";
.tbList {
border-spacing: 0;
border: 1px solid #eee;
width: 100%;
font-size: 14px;
display: flex;
flex-direction: column;
// table-layout: fixed;
user-select: none;
.trh{
background-color: #eee;
font-weight: 600;
td{
// background-color: red;
}
}
.tr {
display: flex;
align-items: center;
// line-height: 1.8;
&:nth-child(2n + 1):not(.trh) {
background-color: #f6f6f6;
}
td {
padding: 6px 6px;
// display: flex;
// align-items: center;
}
&:hover {
background-color: #eee;
.name .mn {
display: block;
}
}
.icon {
width: 20px;
}
.name {
flex: 3;
.text-el-line-normal();
display: flex;
align-items: center;
.ntext {
.text-el-line-normal();
}
.mn {
flex: 1;
display: none;
}
}
.ar {
flex: 2;
.text-el-line-normal();
}
.ar,
.al,
.dt {
font-size: 12px;
}
.al,
.dt {
color: #999;
}
.al {
flex: 1;
.text-el-line-normal();
}
.dt {
width: 50px;
}
}
}
.wp-list {
// position: absolute;
// bottom: 0px;
// top: 80px;
// right: 0;
// left: 160px;
overflow-y: auto;
.tr {
display: flex;
font-size: 14px;
border-top: 1px #eee solid;
border-bottom: 1px #eee solid;
margin-bottom: -1px;
align-items: center;
> * {
padding: 4px 6px;
}
&:nth-child(2n + 1) {
background-color: #f6f6f6;
}
&:hover {
background-color: #eee;
.name .mn {
display: block;
}
}
.icon {
padding-left: 10px;
width: 20px;
}
.name {
flex: 3;
.text-el-line();
display: flex;
align-items: center;
.ntext {
.text-el-line();
}
.mn {
flex: 1;
display: none;
}
}
.ar {
width: 120px;
// flex: 2;
// color: #999;
margin-top: 1px;
font-size: 12px;
.text-el-line();
}
.al {
width: 80px;
// flex: 1;
margin-top: 1px;
color: #999;
font-size: 12px;
.text-el-line();
}
.dt {
margin-top: 1px;
color: #999;
font-size: 12px;
}
}
}
</style>

View File

@ -0,0 +1,60 @@
<script setup>
import {
ref,
reactive,
h,
watch,
toRaw,
onMounted,
onUnmounted,
nextTick,
} from "vue";
import { RouterLink, useRoute, useRouter } from "vue-router";
import { useStore } from "vuex";
import {
NButton,
NButtonGroup,
NSpace,
NIcon,
NDropdown,
NMenu,
NLayout,
NLayoutHeader,
NLayoutFooter,
NLayoutContent,
NLayoutSider,
NTag,
NDataTable,
useMessage,
} from "naive-ui";
import Play from "@/assets/svgs/Play_.svg";
import Pause from "@/assets/svgs/Pause.svg";
import svgDots from "@/assets/svgs/Dots.svg";
import dayjs from "dayjs";
import "dayjs/locale/zh-cn";
import duration from "dayjs/plugin/duration";
import pubsub from "pubsub-js";
import ArtistsSpan from "@/components/ArtistsSpan.vue";
const store = useStore();
const router = useRouter();
dayjs.extend(duration);
const props = defineProps({
songs: Array,
});
</script>
<template>
{{songs}}
</template>
<script>
export default {
}
</script>
<style>
</style>

46
src/network/search.js Normal file
View File

@ -0,0 +1,46 @@
import {request} from './request'
// 搜索默认关键词
export function getDefault(){
return request({
url: '/search/default'
})
}
// 热搜榜简略
export function getSearchHot(){
return request({
url: '/search/hot'
})
}
// 热搜榜详情
export function getHotDetail(){
return request({
url: '/search/hot/detail'
})
}
// 搜索结果
export function searchResult(keywords, limit, offset, type){
return request({
url: '/search',
params: {
keywords,
limit,
offset,
type
}
})
}
// 搜索建议
export function searchSuggest(keywords, type){
return request({
url: '/search/suggest',
params: {
keywords,
type
}
})
}

View File

@ -88,6 +88,15 @@ const routes = [
keepAlive: true,
},
},
{
path: "/search/:type/:keywords",
name: "search",
component: ()=> import('@/views/SearchResult.vue'),
meta:{
keepAlive: false,
},
props: true,
},
{
path: "/friends",
name: "friends",

View File

@ -5,6 +5,7 @@ export default createStore({
// appVersion: "0.0.1",
// debugStr: "测试debug字符",
showSongDetail: false, //是否显示歌曲详情
keywords: '', //查询关键字
settings: {
currentRoute: "/discover/recommend", //当前路由
songId: 0, //歌曲id
@ -12,6 +13,7 @@ export default createStore({
playMode: 0, //播放模式0-3顺序循环单曲随机。
lastPlayed: [], //最近播放
playingList: [], //当前播放
searchHistory: [], //搜索历史
},
caches: {},
theme: {

157
src/views/SearchResult.vue Normal file
View File

@ -0,0 +1,157 @@
<script setup>
import { ref, onActivated, watch } from "vue";
import { searchResult } from "../network/search";
import Songlist from "../components/Songlist.vue";
import SongsList from "../components/SongsList.vue";
import { useStore } from 'vuex';
const props = defineProps({
type: String,
keywords: String,
});
const store = useStore()
onActivated(() => {});
const type = ref(props.type);
const keywords = ref(props.keywords);
const count = ref(0);
const things = ref("单曲");
const songs = ref([]);
watch(
() => [props.type, props.keywords],
([t, k]) => {
search(t, k);
}
// { immediate: true }
);
const search = (t, k) => {
type.value = t;
keywords.value = k;
switch (t) {
case "1":
default:
type.value = "1";
things.value = "单曲";
break;
}
if (keywords.value.length > 0) {
searchResult(k, 20, 0, t)
.then((res) => {
if (res.data.code == 200) {
if (type.value == 1) {
count.value = res.data.result.songCount;
songs.value = res.data.result.songs;
console.log(songs.value);
}
}
})
.catch((err) => {
console.log("searchResult err ", err);
});
}
};
const selStyle = (t) => {
if(t == type.value){
const {primaryColor} = store.state.theme.themeOverrides.common
return {
color: primaryColor,
borderBottom: 'solid 2px ' + primaryColor,
}}
}
</script>
<template>
<div class="main-content">
<div class="lmt-width">
<div class="title">
{{ keywords }}
<span class="result">找到 {{ count }} {{ things }}</span>
</div>
<div class="tabs">
<div class="tab">
<div class="btns">
<span class="caption" :style="selStyle('1')">
单曲
</span>
<span class="caption" :style="selStyle('10')">
专辑
</span>
<span class="caption" :style="selStyle('100')">
歌手
</span>
<span class="caption" :style="selStyle('1000')">
歌单
</span>
<span class="caption" :style="selStyle('1009')">
电台
</span>
<span class="caption" :style="selStyle('1004')">
MV
</span>
<span class="caption" :style="selStyle('1014')">
视频
</span>
</div>
<div class="bt"></div>
</div>
<div class="panel" v-show="type == '1'">
<Songlist :songs="songs"></Songlist>
</div>
<div class="panel" v-show="type == '10'"></div>
<div class="panel" v-show="type == '100'"></div>
<div class="panel" v-show="type == '1000'"></div>
</div>
</div>
</div>
</template>
<script>
export default {};
</script>
<style lang="less" scoped>
@import "@/assets/css/common.less";
.lmt-width {
padding: 0 12px;
.title {
font-size: 30px;
.result {
font-size: 13px;
color: #999;
}
}
.tabs {
.tab {
.btns {
font-size: 16px;
display: flex;
.caption {
padding: 6px 6px;
margin-right: 16px;
color: #666;
border-bottom: solid 2px #f0f0f0;
}
// .sel {
// color: red;
// border-bottom: solid 2px red;
// }
}
.bt{
margin-top: -2px;
height: 2px;
background-color: #f0f0f0;
}
}
}
}
</style>

View File

@ -1,19 +1,63 @@
<script setup>
import {NInput} from 'naive-ui'
import { NInput, NIcon } from "naive-ui";
import pubsub from "pubsub-js";
import { useRouter } from "vue-router";
import { useStore } from "vuex";
import { getHotDetail, searchSuggest } from "@/network/search.js";
import { ref } from "vue";
import SvgSearchOutline from "@/assets/svgs/SearchOutline.svg";
const store = useStore();
const router = useRouter();
const keywords = ref("");
const elSearch = ref(null);
const search = () => {
if (keywords.value.length > 0){
pubsub.publish("zp.toggleSearch");
// elSearch.value.blur()
router.push(`/search/1/${keywords.value}`);
}
};
const handleInput = () => {
pubsub.publish('zp.showSearch');
pubsub.publish("zp.searchInput", keywords.value);
};
const result = ref({});
</script>
<template>
<div id="search">
<n-input size="small" round placeholder="请搜索..." />
<n-input
ref="elSearch"
size="small"
round
placeholder="请搜索..."
clearable
@activate="
() => {
pubsub.publish('zp.showSearch');
}
"
v-model:value="keywords"
@keyup.enter="search"
@input="handleInput"
style="width: 190px"
>
<template #prefix>
<n-icon>
<SvgSearchOutline />
</n-icon>
</template>
</n-input>
</div>
</template>
<script>
export default {
}
export default {};
</script>
<style>
</style>
<style></style>

View File

@ -0,0 +1,226 @@
<script setup>
import { useStore } from "vuex";
import { getHotDetail, searchSuggest } from "@/network/search.js";
import { ref, onUnmounted } from "vue";
import { NTag, NButton, NSpace } from "naive-ui";
import pubsub from "pubsub-js";
const store = useStore();
//#region
const hotSearch = ref([]);
hotSearch.value = store.getters.cache("hotSearch");
// if(store.getters.cache('topSongs'))
// {
// topSongs.value = store.getters.cache('topSongs')
// console.log('Caches');
// }
//
getHotDetail()
.then((res) => {
if (res.data.code == 200) {
hotSearch.value = res.data.data;
store.commit("saveCaches", {
hotSearch: { data: hotSearch.value, time: Date.now() },
});
} else {
}
// console.log(hotSearch.value);
})
.catch((err) => {
console.log("getHotDetail err", err);
});
//#endregion
const suggestResult = ref({});
const suggest = (keywords) => {
if (keywords == "") return;
searchSuggest(keywords)
.then((res) => {
suggestResult.value = res.data.result;
})
.catch((err) => {
console.log("suggest err", err);
});
};
const keywords = ref("");
const token = pubsub.subscribe("zp", (msg, data) => {
switch (msg) {
case "zp.searchInput":
keywords.value = data;
suggest(keywords.value);
break;
}
});
//
onUnmounted(() => {
pubsub.unsubscribe(token);
});
</script>
<template>
<div class="hot" v-if="keywords.length < 1">
<div
class="history"
v-if="store.state.settings.searchHistory?.length > 0"
>
<div class="caption">
<span class="txt">搜索历史</span>
<NButton round size="tiny" type="error">清除</NButton>
</div>
<NSpace class="h-list" :size="[6, 6]">
<NTag
v-for="h in store.state.settings.searchHistory"
closable
round
size="small"
type="primary"
>{{ h }}</NTag
>
</NSpace>
</div>
<div class="hot-search">
<div class="caption">热搜榜</div>
<div
class="hot-list"
v-for="(item, idx) in hotSearch"
key="idx"
>
<div class="idx" :class="{ idxHot: item.iconType == 1 }">
{{ idx + 1 }}
</div>
<div class="detail">
<div class="title">
<span class="word">{{ item.searchWord }}</span>
<span class="isHot" v-if="item.iconType == 1">HOT</span>
<span class="score">{{ item.score }}</span>
</div>
<div class="content">{{ item.content }}</div>
</div>
</div>
</div>
</div>
<div class="searching" v-else>
<div class="to-search">{{ keywords }}相关的结果 ></div>
<div class="suggest">
<div class="caption">单曲</div>
<div
class="list"
v-for="s in suggestResult.songs"
@click="() => pubsub.publish('zp.play', {id: s.id, im: true})"
>
{{ s.name }} -
<template v-for="(ar, idx) of s.artists" key="idx">
<span style="margin-right: 4px; cursor: pointer">{{
ar.name
}}</span>
</template>
</div>
<div class="caption">歌手</div>
<div class="list" v-for="s in suggestResult.artists">
{{ s.name }}
</div>
<div class="caption">专辑</div>
<div class="list" v-for="s in suggestResult.albums">
{{ s.name }}
<template v-for="(ar, idx) of s.artists" key="idx">
<span style="margin-right: 4px; cursor: pointer">{{
ar.name
}}</span>
</template>
</div>
<div class="caption">歌单</div>
<div class="list" v-for="s in suggestResult.albums">
{{ s.name }}
</div>
</div>
</div>
</template>
<script>
export default {};
</script>
<style lang="less" scoped>
@import "@/assets/css/common.less";
.hot {
padding: 8px;
.history {
margin-bottom: 6px;
.caption {
display: flex;
padding-bottom: 4px;
.txt {
flex: 1;
}
}
.h-list {
}
}
.hot-search {
.hot-list {
display: flex;
align-items: center;
color: #999;
.idx {
padding: 6px 12px;
width: 30px;
justify-items: end;
text-align: right;
}
.idxHot {
color: red;
}
.detail {
padding: 6px;
.title {
.text-el-line();
& > * {
margin-right: 6px;
}
.word {
color: #333;
font-size: 14px;
}
.isHot {
font-size: 12px;
color: red;
}
.score {
font-size: 12px;
}
}
.content {
font-size: 12px;
.text-el-line();
}
}
}
}
}
.searching {
padding: 8px;
.suggest {
font-size: 13px;
line-height: 1.8;
.caption {
color: #999;
.text-el-line();
}
.list {
padding-left: 20px;
cursor: pointer;
.text-el-line();
&:hover {
background: #eee;
}
}
}
}
</style>