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

View File

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

View File

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

View File

@ -1,6 +1,9 @@
<template>
<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>
<CoverRow
:type="'playlist'"
@ -147,6 +150,9 @@ export default {
.index-row {
margin-top: 54px;
}
.index-row.first-row {
margin-top: 32px;
}
.playlists {
display: flex;
flex-wrap: wrap;

View File

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

View File

@ -1,92 +1,94 @@
<template>
<div class="login">
<div class="section-1">
<img src="/img/logos/netease-music.png" />
</div>
<div class="title">{{ $t('login.loginText') }}</div>
<div class="section-2">
<div v-show="mode === 'phone'" class="input-box">
<div class="container" :class="{ active: inputFocus === 'phone' }">
<svg-icon icon-class="mobile" />
<div class="inputs">
<input
id="countryCode"
v-model="countryCode"
:placeholder="
inputFocus === 'phone' ? '' : $t('login.countryCode')
"
@focus="inputFocus = 'phone'"
@blur="inputFocus = ''"
@keyup.enter="login"
/>
<input
id="phoneNumber"
v-model="phoneNumber"
:placeholder="inputFocus === 'phone' ? '' : $t('login.phone')"
@focus="inputFocus = 'phone'"
@blur="inputFocus = ''"
@keyup.enter="login"
/>
<div class="login-container">
<div class="section-1">
<img src="/img/logos/netease-music.png" />
</div>
<div class="title">{{ $t('login.loginText') }}</div>
<div class="section-2">
<div v-show="mode === 'phone'" class="input-box">
<div class="container" :class="{ active: inputFocus === 'phone' }">
<svg-icon icon-class="mobile" />
<div class="inputs">
<input
id="countryCode"
v-model="countryCode"
:placeholder="
inputFocus === 'phone' ? '' : $t('login.countryCode')
"
@focus="inputFocus = 'phone'"
@blur="inputFocus = ''"
@keyup.enter="login"
/>
<input
id="phoneNumber"
v-model="phoneNumber"
:placeholder="inputFocus === 'phone' ? '' : $t('login.phone')"
@focus="inputFocus = 'phone'"
@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 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 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="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 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 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>
</template>
@ -206,7 +208,14 @@ export default {
flex-direction: column;
align-items: 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 {
@ -223,6 +232,7 @@ export default {
img {
height: 64px;
margin: 20px;
user-select: none;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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