feat: virtual scrollbar

This commit is contained in:
qier222 2021-06-05 14:16:53 +08:00
parent 226a2145c4
commit 7c79afd0d1
No known key found for this signature in database
GPG Key ID: 9C85007ED905F14D
13 changed files with 341 additions and 133 deletions

View File

@ -1,7 +1,8 @@
<template> <template>
<div id="app"> <div id="app" :class="{ 'user-select-none': userSelectNone }">
<Scrollbar v-show="!showLyrics" ref="scrollbar" />
<Navbar v-show="showNavbar" ref="navbar" /> <Navbar v-show="showNavbar" ref="navbar" />
<main> <main ref="main" @scroll="handleScroll">
<keep-alive> <keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view> <router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive> </keep-alive>
@ -22,6 +23,7 @@
<script> <script>
import ModalAddTrackToPlaylist from './components/ModalAddTrackToPlaylist.vue'; import ModalAddTrackToPlaylist from './components/ModalAddTrackToPlaylist.vue';
import ModalNewPlaylist from './components/ModalNewPlaylist.vue'; import ModalNewPlaylist from './components/ModalNewPlaylist.vue';
import Scrollbar from './components/Scrollbar.vue';
import Navbar from './components/Navbar.vue'; import Navbar from './components/Navbar.vue';
import Player from './components/Player.vue'; import Player from './components/Player.vue';
import Toast from './components/Toast.vue'; import Toast from './components/Toast.vue';
@ -39,10 +41,12 @@ export default {
ModalAddTrackToPlaylist, ModalAddTrackToPlaylist,
ModalNewPlaylist, ModalNewPlaylist,
Lyrics, Lyrics,
Scrollbar,
}, },
data() { data() {
return { return {
isElectron: process.env.IS_ELECTRON, // true || undefined isElectron: process.env.IS_ELECTRON, // true || undefined
userSelectNone: false,
}; };
}, },
computed: { computed: {
@ -93,6 +97,9 @@ export default {
this.$store.dispatch('fetchLikedMVs'); this.$store.dispatch('fetchLikedMVs');
} }
}, },
handleScroll() {
this.$refs.scrollbar.handleScroll();
},
}, },
}; };
</script> </script>
@ -104,20 +111,18 @@ export default {
} }
main { main {
margin-top: 84px; position: fixed;
margin-bottom: 96px; top: 0;
padding: { bottom: 0;
right: 10vw; right: 0;
left: 10vw; left: 0;
} overflow: auto;
padding: 64px 10vw 96px 10vw;
box-sizing: border-box;
} }
@media (max-width: 1336px) { @media (max-width: 1336px) {
main { main {
padding: 0 5vw;
}
}
padding: 64px 5vw 96px 5vw; padding: 64px 5vw 96px 5vw;
} }
} }

View File

@ -0,0 +1,173 @@
<template>
<div>
<transition name="fade">
<div
v-show="show"
id="scrollbar"
:class="{ 'on-drag': isOnDrag }"
@click="handleClick"
>
<div
id="thumbContainer"
:class="{ active }"
:style="thumbStyle"
@mouseenter="handleMouseenter"
@mouseleave="handleMouseleave"
@mousedown="handleDragStart"
@click.stop
>
<div></div>
</div>
</div>
</transition>
</div>
</template>
<script>
export default {
name: 'Scrollbar',
data() {
return {
top: 0,
thumbHeight: 0,
active: false,
show: false,
hideTimer: null,
isOnDrag: false,
onDragClientY: 0,
};
},
computed: {
thumbStyle() {
return {
transform: `translateY(${this.top}px)`,
height: `${this.thumbHeight}px`,
};
},
main() {
return this.$parent.$refs.main;
},
},
created() {
this.$router.beforeEach((to, from, next) => {
this.show = false;
next();
});
},
methods: {
handleScroll() {
const clintHeight = this.main.clientHeight - 128;
const scrollHeight = this.main.scrollHeight - 128;
const scrollTop = this.main.scrollTop;
let top = ~~((scrollTop / scrollHeight) * clintHeight);
let thumbHeight = ~~((clintHeight / scrollHeight) * clintHeight);
if (thumbHeight < 24) thumbHeight = 24;
if (top > clintHeight - thumbHeight) {
top = clintHeight - thumbHeight;
}
this.top = top;
this.thumbHeight = thumbHeight;
if (!this.show && clintHeight !== thumbHeight) this.show = true;
this.setScrollbarHideTimeout();
},
handleMouseenter() {
this.active = true;
},
handleMouseleave() {
this.active = false;
this.setScrollbarHideTimeout();
},
handleDragStart(e) {
this.onDragClientY = e.clientY;
this.isOnDrag = true;
this.$parent.userSelectNone = true;
document.addEventListener('mousemove', this.handleDragMove);
document.addEventListener('mouseup', this.handleDragEnd);
},
handleDragMove(e) {
if (!this.isOnDrag) return;
const clintHeight = this.main.clientHeight - 128;
const scrollHeight = this.main.scrollHeight - 128;
const clientY = e.clientY;
const scrollTop = this.main.scrollTop;
const offset = ~~(
((clientY - this.onDragClientY) / clintHeight) *
scrollHeight
);
this.top = ~~((scrollTop / scrollHeight) * clintHeight);
this.main.scrollBy(0, offset);
this.onDragClientY = clientY;
},
handleDragEnd() {
this.isOnDrag = false;
this.$parent.userSelectNone = false;
document.removeEventListener('mousemove', this.handleDragMove);
document.removeEventListener('mouseup', this.handleDragEnd);
},
handleClick() {
this.main.scrollBy({
top: 256,
behavior: 'smooth',
});
},
setScrollbarHideTimeout() {
if (this.hideTimer !== null) clearTimeout(this.hideTimer);
this.hideTimer = setTimeout(() => {
if (!this.active) this.show = false;
this.hideTimer = null;
}, 4000);
},
},
};
</script>
<style lang="scss" scoped>
#scrollbar {
position: fixed;
right: 0;
top: 0;
bottom: 0;
width: 16px;
z-index: 1000;
#thumbContainer {
margin-top: 64px;
div {
transition: background 0.4s;
position: absolute;
right: 2px;
width: 8px;
height: 100%;
border-radius: 4px;
background: rgba(128, 128, 128, 0.38);
}
}
#thumbContainer.active div {
background: rgba(128, 128, 128, 0.58);
}
}
[data-theme='dark'] {
#thumbContainer div {
background: var(--color-secondary-bg);
}
}
#scrollbar.on-drag {
left: 0;
width: auto;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div v-show="show" class="album"> <div v-show="show" class="album-page">
<div class="playlist-info"> <div class="playlist-info">
<Cover <Cover
:id="album.id" :id="album.id"
@ -299,6 +299,9 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.album-page {
margin-top: 32px;
}
.playlist-info { .playlist-info {
display: flex; display: flex;
width: 78vw; width: 78vw;

View File

@ -1,5 +1,5 @@
<template> <template>
<div v-show="show" class="artist"> <div v-show="show" class="artist-page">
<div class="artist-info"> <div class="artist-info">
<div class="head"> <div class="head">
<img :src="artist.img1v1Url | resizeImage(1024)" /> <img :src="artist.img1v1Url | resizeImage(1024)" />
@ -354,6 +354,10 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.artist-page {
margin-top: 32px;
}
.artist-info { .artist-info {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="explore"> <div class="explore-page">
<h1>{{ $t('explore.explore') }}</h1> <h1>{{ $t('explore.explore') }}</h1>
<div class="buttons"> <div class="buttons">
<div <div

View File

@ -1,6 +1,9 @@
<template> <template>
<div v-show="show" class="home"> <div v-show="show" class="home">
<div v-if="settings.showPlaylistsByAppleMusic !== false" class="index-row"> <div
v-if="settings.showPlaylistsByAppleMusic !== false"
class="index-row first-row"
>
<div class="title"> by Apple Music </div> <div class="title"> by Apple Music </div>
<CoverRow <CoverRow
:type="'playlist'" :type="'playlist'"
@ -147,6 +150,9 @@ export default {
.index-row { .index-row {
margin-top: 54px; margin-top: 54px;
} }
.index-row.first-row {
margin-top: 32px;
}
.playlists { .playlists {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

@ -1,5 +1,5 @@
<template> <template>
<div v-show="show"> <div v-show="show" ref="library">
<h1> <h1>
<img class="avatar" :src="data.user.avatarUrl | resizeImage" />{{ <img class="avatar" :src="data.user.avatarUrl | resizeImage" />{{
data.user.nickname data.user.nickname
@ -123,7 +123,7 @@
</div> </div>
</div> </div>
<ContextMenu ref="playlistTabMenu"> <ContextMenu>
<div class="item" @click="changePlaylistFilter('all')">{{ <div class="item" @click="changePlaylistFilter('all')">{{
$t('contextMenu.allPlaylists') $t('contextMenu.allPlaylists')
}}</div> }}</div>
@ -241,7 +241,7 @@ export default {
return; return;
} }
this.currentTab = tab; this.currentTab = tab;
window.scrollTo({ top: 375, behavior: 'smooth' }); this.$parent.$refs.main.scrollTo({ top: 375, behavior: 'smooth' });
}, },
goToLikedSongsList() { goToLikedSongsList() {
this.$router.push({ path: '/library/liked-songs' }); this.$router.push({ path: '/library/liked-songs' });

View File

@ -1,92 +1,94 @@
<template> <template>
<div class="login"> <div class="login">
<div class="section-1"> <div class="login-container">
<img src="/img/logos/netease-music.png" /> <div class="section-1">
</div> <img src="/img/logos/netease-music.png" />
<div class="title">{{ $t('login.loginText') }}</div> </div>
<div class="section-2"> <div class="title">{{ $t('login.loginText') }}</div>
<div v-show="mode === 'phone'" class="input-box"> <div class="section-2">
<div class="container" :class="{ active: inputFocus === 'phone' }"> <div v-show="mode === 'phone'" class="input-box">
<svg-icon icon-class="mobile" /> <div class="container" :class="{ active: inputFocus === 'phone' }">
<div class="inputs"> <svg-icon icon-class="mobile" />
<input <div class="inputs">
id="countryCode" <input
v-model="countryCode" id="countryCode"
:placeholder=" v-model="countryCode"
inputFocus === 'phone' ? '' : $t('login.countryCode') :placeholder="
" inputFocus === 'phone' ? '' : $t('login.countryCode')
@focus="inputFocus = 'phone'" "
@blur="inputFocus = ''" @focus="inputFocus = 'phone'"
@keyup.enter="login" @blur="inputFocus = ''"
/> @keyup.enter="login"
<input />
id="phoneNumber" <input
v-model="phoneNumber" id="phoneNumber"
:placeholder="inputFocus === 'phone' ? '' : $t('login.phone')" v-model="phoneNumber"
@focus="inputFocus = 'phone'" :placeholder="inputFocus === 'phone' ? '' : $t('login.phone')"
@blur="inputFocus = ''" @focus="inputFocus = 'phone'"
@keyup.enter="login" @blur="inputFocus = ''"
/> @keyup.enter="login"
/>
</div>
</div>
</div>
<div v-show="mode === 'email'" class="input-box">
<div class="container" :class="{ active: inputFocus === 'email' }">
<svg-icon icon-class="mail" />
<div class="inputs">
<input
id="email"
v-model="email"
type="email"
:placeholder="inputFocus === 'email' ? '' : $t('login.email')"
@focus="inputFocus = 'email'"
@blur="inputFocus = ''"
@keyup.enter="login"
/>
</div>
</div>
</div>
<div class="input-box">
<div class="container" :class="{ active: inputFocus === 'password' }">
<svg-icon icon-class="lock" />
<div class="inputs">
<input
id="password"
v-model="password"
type="password"
:placeholder="
inputFocus === 'password' ? '' : $t('login.password')
"
@focus="inputFocus = 'password'"
@blur="inputFocus = ''"
@keyup.enter="login"
/>
</div>
</div> </div>
</div> </div>
</div> </div>
<div v-show="mode === 'email'" class="input-box"> <div class="confirm">
<div class="container" :class="{ active: inputFocus === 'email' }"> <button v-show="!processing" @click="login">
<svg-icon icon-class="mail" /> {{ $t('login.login') }}
<div class="inputs"> </button>
<input <button v-show="processing" class="loading" disabled>
id="email" <span></span>
v-model="email" <span></span>
type="email" <span></span>
:placeholder="inputFocus === 'email' ? '' : $t('login.email')" </button>
@focus="inputFocus = 'email'"
@blur="inputFocus = ''"
@keyup.enter="login"
/>
</div>
</div>
</div> </div>
<div class="input-box"> <div class="other-login">
<div class="container" :class="{ active: inputFocus === 'password' }"> <a v-show="mode === 'phone'" @click="mode = 'email'">{{
<svg-icon icon-class="lock" /> $t('login.loginWithEmail')
<div class="inputs"> }}</a>
<input <a v-show="mode === 'email'" @click="mode = 'phone'">{{
id="password" $t('login.loginWithPhone')
v-model="password" }}</a>
type="password"
:placeholder="
inputFocus === 'password' ? '' : $t('login.password')
"
@focus="inputFocus = 'password'"
@blur="inputFocus = ''"
@keyup.enter="login"
/>
</div>
</div>
</div> </div>
<div
class="notice"
v-html="isElectron ? $t('login.noticeElectron') : $t('login.notice')"
></div>
</div> </div>
<div class="confirm">
<button v-show="!processing" @click="login">
{{ $t('login.login') }}
</button>
<button v-show="processing" class="loading" disabled>
<span></span>
<span></span>
<span></span>
</button>
</div>
<div class="other-login">
<a v-show="mode === 'phone'" @click="mode = 'email'">{{
$t('login.loginWithEmail')
}}</a>
<a v-show="mode === 'email'" @click="mode = 'phone'">{{
$t('login.loginWithPhone')
}}</a>
</div>
<div
class="notice"
v-html="isElectron ? $t('login.noticeElectron') : $t('login.notice')"
></div>
</div> </div>
</template> </template>
@ -206,7 +208,14 @@ export default {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: calc(100vh - 192px); margin-top: 32px;
}
.login-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
} }
.title { .title {
@ -223,6 +232,7 @@ export default {
img { img {
height: 64px; height: 64px;
margin: 20px; margin: 20px;
user-select: none;
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="mv"> <div class="mv-page">
<div class="current-video"> <div class="current-video">
<div class="video"> <div class="video">
<video ref="videoPlayer" class="plyr"></video> <video ref="videoPlayer" class="plyr"></video>
@ -136,8 +136,9 @@ export default {
--plyr-control-radius: 8px; --plyr-control-radius: 8px;
} }
.mv { .mv-page {
width: 100%; width: 100%;
margin-top: 32px;
} }
.current-video { .current-video {
width: 100%; width: 100%;
@ -176,6 +177,7 @@ export default {
font-weight: 600; font-weight: 600;
color: var(--color-text); color: var(--color-text);
opacity: 0.88; opacity: 0.88;
margin-bottom: 12px;
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div v-show="show"> <div v-show="show" class="playlist">
<div <div
v-if="specialPlaylistInfo === undefined && !isLikeSongsPage" v-if="specialPlaylistInfo === undefined && !isLikeSongsPage"
class="playlist-info" class="playlist-info"
@ -174,6 +174,16 @@
" "
/> />
<div class="load-more">
<ButtonTwoTone
v-show="hasMore"
color="grey"
:loading="loadingMore"
@click.native="loadMore(100)"
>{{ $t('explore.loadMore') }}</ButtonTwoTone
>
</div>
<Modal <Modal
:show="showFullDescription" :show="showFullDescription"
:close="toggleFullDescription" :close="toggleFullDescription"
@ -346,6 +356,7 @@ export default {
showFullDescription: false, showFullDescription: false,
tracks: [], tracks: [],
loadingMore: false, loadingMore: false,
hasMore: false,
lastLoadedTrackIndex: 9, lastLoadedTrackIndex: 9,
displaySearchInPlaylist: false, // displaySearchInPlaylist: false, //
searchKeyWords: '', // 使 searchKeyWords: '', // 使
@ -397,9 +408,6 @@ export default {
this.loadData(this.$route.params.id); this.loadData(this.$route.params.id);
} }
}, },
destroyed() {
window.removeEventListener('scroll', this.handleScroll, true);
},
methods: { methods: {
...mapMutations(['appendTrackToPlayerList']), ...mapMutations(['appendTrackToPlayerList']),
...mapActions(['playFirstTrackOnList', 'playTrackOnListByID', 'showToast']), ...mapActions(['playFirstTrackOnList', 'playTrackOnListByID', 'showToast']),
@ -443,9 +451,6 @@ export default {
if (next !== undefined) next(); if (next !== undefined) next();
this.show = true; this.show = true;
this.lastLoadedTrackIndex = data.playlist.tracks.length - 1; this.lastLoadedTrackIndex = data.playlist.tracks.length - 1;
if (this.playlist.trackCount > this.tracks.length) {
window.addEventListener('scroll', this.handleScroll, true);
}
return data; return data;
}) })
.then(() => { .then(() => {
@ -455,37 +460,27 @@ export default {
} }
}); });
}, },
loadMore(loadNum = 50) { loadMore(loadNum = 100) {
let trackIDs = this.playlist.trackIds.filter((t, index) => { let trackIDs = this.playlist.trackIds.filter((t, index) => {
if ( if (
index > this.lastLoadedTrackIndex && index > this.lastLoadedTrackIndex &&
index <= this.lastLoadedTrackIndex + loadNum index <= this.lastLoadedTrackIndex + loadNum
) ) {
return t; return t;
}
}); });
trackIDs = trackIDs.map(t => t.id); trackIDs = trackIDs.map(t => t.id);
getTrackDetail(trackIDs.join(',')).then(data => { getTrackDetail(trackIDs.join(',')).then(data => {
this.tracks.push(...data.songs); this.tracks.push(...data.songs);
this.lastLoadedTrackIndex += trackIDs.length; this.lastLoadedTrackIndex += trackIDs.length;
this.loadingMore = false; this.loadingMore = false;
if (this.lastLoadedTrackIndex + 1 === this.playlist.trackIds.length) {
this.hasMore = false;
} else {
this.hasMore = true;
}
}); });
}, },
handleScroll(e) {
let dom = document.querySelector('html');
let scrollHeight = Math.max(dom.scrollHeight, dom.scrollHeight);
let scrollTop = e.target.scrollingElement.scrollTop;
let clientHeight =
dom.innerHeight || Math.min(dom.clientHeight, dom.clientHeight);
if (clientHeight + scrollTop + 200 >= scrollHeight) {
if (
this.lastLoadedTrackIndex + 1 === this.playlist.trackIds.length ||
this.loadingMore
)
return;
this.loadingMore = true;
this.loadMore();
}
},
openMenu(e) { openMenu(e) {
this.$refs.playlistMenu.openMenu(e); this.$refs.playlistMenu.openMenu(e);
}, },
@ -546,6 +541,9 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.playlist {
margin-top: 32px;
}
.playlist-info { .playlist-info {
display: flex; display: flex;
margin-bottom: 72px; margin-bottom: 72px;
@ -936,4 +934,10 @@ export default {
right: 8vw; right: 8vw;
} }
} }
.load-more {
display: flex;
justify-content: center;
margin-top: 32px;
}
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div v-show="show" class="search"> <div v-show="show" class="search-page">
<div v-show="artists.length > 0 || albums.length > 0" class="row"> <div v-show="artists.length > 0 || albums.length > 0" class="row">
<div v-show="artists.length > 0" class="artists"> <div v-show="artists.length > 0" class="artists">
<div v-show="artists.length > 0" class="section-title" <div v-show="artists.length > 0" class="section-title"
@ -237,7 +237,7 @@ export default {
.row { .row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
margin-top: 98px; margin-top: 32px;
.artists { .artists {
flex: 1; flex: 1;

View File

@ -139,7 +139,7 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
h1 { h1 {
margin-top: -10px; margin-top: 32px;
margin-bottom: 28px; margin-bottom: 28px;
color: var(--color-text); color: var(--color-text);
span { span {

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="settings"> <div class="settings-page">
<div class="container"> <div class="container">
<div v-if="showUserInfo" class="user"> <div v-if="showUserInfo" class="user">
<div class="left"> <div class="left">
@ -765,9 +765,10 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.settings { .settings-page {
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-top: 32px;
} }
.container { .container {
margin-top: 24px; margin-top: 24px;