diff --git a/package-lock.json b/package-lock.json index 11757d64d18c6d86d0762896b72369febfb7b80c..051231706d92efdaac854b597e3e9c332b24f35e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "colorthief": "^2.4.0", "core-js": "^3.38.1", "element-plus": "^2.8.2", + "js-md5": "^0.8.3", "pinia": "^2.2.2", "swiper": "^11.1.14", "tippy.js": "^6.3.7", @@ -11167,6 +11168,11 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/js-md5": { + "version": "0.8.3", + "resolved": "https://registry.npmmirror.com/js-md5/-/js-md5-0.8.3.tgz", + "integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==" + }, "node_modules/js-message": { "version": "1.0.7", "resolved": "https://repo.huaweicloud.com/repository/npm/js-message/-/js-message-1.0.7.tgz", @@ -26017,6 +26023,11 @@ "@sideway/pinpoint": "^2.0.0" } }, + "js-md5": { + "version": "0.8.3", + "resolved": "https://registry.npmmirror.com/js-md5/-/js-md5-0.8.3.tgz", + "integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==" + }, "js-message": { "version": "1.0.7", "resolved": "https://repo.huaweicloud.com/repository/npm/js-message/-/js-message-1.0.7.tgz", diff --git a/package.json b/package.json index 0062ab5fd2f86db67b59d73184fd312a866a51ab..190843cc65d8fff7490decaf252b95ef5d97e29d 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "colorthief": "^2.4.0", "core-js": "^3.38.1", "element-plus": "^2.8.2", + "js-md5": "^0.8.3", "pinia": "^2.2.2", "swiper": "^11.1.14", "tippy.js": "^6.3.7", diff --git a/src/api/resolve.js b/src/api/resolve.js index 6be4c21f2c6b3916c436eba146c9037b6c54afea..cf539a9843073c1ceae3767310f0953dbdb4254a 100644 --- a/src/api/resolve.js +++ b/src/api/resolve.js @@ -1,5 +1,5 @@ import {axios} from "@/utils/request"; -import {PLAYLIST_MODULE, RESOLVE_MODULE, SONG_MODULE, USER_MODULE} from "@/api/_prefix"; +import {ARTIST_MODULE, PLAYLIST_MODULE, RESOLVE_MODULE, SONG_MODULE, USER_MODULE} from "@/api/_prefix"; /* // TODO: newly added @@ -35,4 +35,12 @@ export const getPlaylistById = (playlistId) => { .then(res => { return res; }); +} + +export const getArtistById = (artistId) => { + console.log(artistId) + return axios.get(`${ARTIST_MODULE}/${artistId}`) + .then(res => { + return res; + }); } \ No newline at end of file diff --git a/src/api/song.js b/src/api/song.js index 2e4f8fe525b27330240ae059db6c327c5b3999a0..100cc9733c07392eb6fb1c5c08aa2080a25c3c1b 100644 --- a/src/api/song.js +++ b/src/api/song.js @@ -37,4 +37,17 @@ export const getSongsByEpisode = (episodeInfo) => { .then(res => { return res; }); -} \ No newline at end of file +} + +export const getRecommendedSongs = (params) => { + console.log("params" + params.currentSongIds) + return axios.post(`${SONG_MODULE}/recommendations`, null,{ + params: { + currentSongIds: params.currentSongIds.join(","), + limit: params.limit, + } + }) + .then(res => { + return res; + }); +}; \ No newline at end of file diff --git a/src/api/user.js b/src/api/user.js index 56b48273fd04f8a0834cb7a1f97ae791fb64e935..fa4c3861e7982ca35d5880c7cfd88de3f5246f54 100644 --- a/src/api/user.js +++ b/src/api/user.js @@ -1,32 +1,66 @@ import { axios } from '../utils/request'; import { USER_MODULE } from './_prefix'; - /* - email: string + phone: string, password: string */ export const userLogin = (loginInfo) => { - console.log(loginInfo) - return axios.post(`${USER_MODULE}/login`, loginInfo, - { headers: { 'Content-Type': 'application/json' } }) - .then(res => { - return res; - }); + return axios.post(`${USER_MODULE}/login`, null, {params: loginInfo}) + .then((res) => { + return res + }) } /* - user_name: string - email: string - password: string + name: string, + phone: string, + password: string, + captcha: string */ export const userRegister = (registerInfo) => { - console.log(registerInfo) - return axios.post(`${USER_MODULE}/register`, registerInfo, - { headers: { 'Content-Type': 'application/json' } }) - .then(res => { - return res; - }); + return axios.post(`${USER_MODULE}/register?`, null, {params: registerInfo}) + .then((res) => { + return res + }) +} + +/* + phone: string + */ +export const userSendCaptcha = (captchaInfo) => { + console.log(captchaInfo.phone) + return axios.post(`${USER_MODULE}/send`, null, { + params: { + phone: captchaInfo.phone + } + }) + .then((res) => { + return res + }) +} + +/* + phone?: string, + password?: string, + captcha?: string, + */ +export const userReset = (resetInfo) => { + console.log(resetInfo) + return axios.post(`${USER_MODULE}/reset`, resetInfo, {headers: {'Content-Type': 'application/json'}}) + .then((res) => { + return res + }) +} + +/* + phone:string + */ +export const getUserByPhone = (phone) => { + return axios.get(`${USER_MODULE}/getUserByPhone`, {params: phone}) + .then((res) => { + return res + }) } /* diff --git a/src/assets/videos/3.mp4 b/src/assets/videos/3.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..3fb3ebe9f7f1e5b4e15272daa19f33e308e04f5b Binary files /dev/null and b/src/assets/videos/3.mp4 differ diff --git a/src/components/ArtistView.vue b/src/components/ArtistView.vue index b2b786848d8c6cdc9fe61de18001283c7c390dff..df16920d597ae859ac2d8257b59203309b76fda2 100644 --- a/src/components/ArtistView.vue +++ b/src/components/ArtistView.vue @@ -10,9 +10,8 @@ import {addSongToPlaylist, removeSongFromPlaylist} from "../api/playlist"; import {getSongsByPlaylist} from "../api/song"; import {formatTime} from '../utils/formatTime'; import {loadSongDurations} from '../utils/loadSongDurations'; -import {userFollowArtist} from "@/api/user"; -const emit = defineEmits(['playSong', 'pauseSong', 'back', 'updateSongs']); +const emit = defineEmits(['playSong', 'pauseSong', 'back', 'updateSongs', 'toggleFollow']); const props = defineProps({ artistName: { type: String, @@ -24,6 +23,9 @@ const props = defineProps({ currentSongId: { type: Number, required: true + }, + isFollowed: { + type: Boolean } }); @@ -40,22 +42,12 @@ let musicClickedIndex = ref(null); let musicPlayIndex = ref(null); let musicPauseIndex = ref(null); -// 娣诲姞鍏虫敞鐘舵€� -const isFollowing = ref(false); - // 鐢ㄦ埛绗竴娆℃挱鏀捐鑹轰汉姝屾洸鐨勬爣蹇� const isFirstPlay = ref(true); // 鍏虫敞/鍙栨秷鍏虫敞閫昏緫 const toggleFollow = () => { - userFollowArtist({ - user_id: currentUserId.value, - artist_id: artist.value.id, - isFollowed: isFollowing.value - }).catch(error => { - console.error("Failed to update artist follow status:", error); - }) - isFollowing.value = !isFollowing.value; + emit('toggleFollow',artist.value.id,props.isFollowed); }; // 娣诲姞鍠滄姝屾洸鐨勭姸鎬佺鐞� @@ -155,12 +147,8 @@ const getRandomListeners = () => { const monthlyListeners = ref(getRandomListeners()); -const followedArtistIds = ref([]); onMounted(() => { - getUserById({userId: currentUserId.value}).then(res => { - followedArtistIds.value = res.data.result.followedArtistIds; - }) // 鍒濆鍖栧枩娆㈢殑姝屾洸闆嗗悎 initializeLikedSongs(); @@ -190,11 +178,6 @@ watch(() => props.artistName, async (newValue) => { const artistResponse = await getArtistInfo(newValue); artist.value = artistResponse.data.result; - if (!followedArtistIds.value.length) { - const userResponse = await getUserById({ userId: currentUserId.value }); - followedArtistIds.value = userResponse.data.result.followedArtistIds; - } - isFollowing.value = followedArtistIds.value.includes(artist.value.id); updateBackground(artist.value.avatarUrl); const songPromises = artist.value.songIds.map(songId => @@ -372,9 +355,9 @@ const isCurrentSongInList = computed(() => { </div> <!-- 鏇挎崲鍘熸潵鐨別l-popover涓哄叧娉ㄦ寜閽� --> <button class="follow-button" - :class="{ 'following': isFollowing }" + :class="{ 'following': props.isFollowed }" @click="toggleFollow"> - {{ isFollowing ? '宸插叧娉�' : '鍏虫敞' }} + {{ props.isFollowed ? '宸插叧娉�' : '鍏虫敞' }} </button> </div> diff --git a/src/components/EpisodeView.vue b/src/components/EpisodeView.vue index bf326f1f41f526b4812c3c533c1194c8d29b4c6d..6cc32220a8155a4e40053137bb4ef15e82f1a80f 100644 --- a/src/components/EpisodeView.vue +++ b/src/components/EpisodeView.vue @@ -292,7 +292,12 @@ const pauseMusic = (musicId) => { <template> <div class="album-content" :style="{backgroundImage: gradientColor}" @mousewheel="handelScroll"> - <div class="header"> + <div class="back-button" data-tooltip="杩斿洖" @click="$emit('back')"> + <svg height="16" width="16" viewBox="0 0 16 16" fill="currentColor"> + <path d="M11.03.47a.75.75 0 0 1 0 1.06L4.56 8l6.47 6.47a.75.75 0 1 1-1.06 1.06L2.44 8 9.97.47a.75.75 0 0 1 1.06 0z"></path> + </svg> + </div> + <div class="header"> <img :src="episodeInfo.picPath" alt="" class="album-image" @load="updateBackground(episodeInfo.picPath)"/> <div class="header-content"> <p style="text-align: left;margin:20px 0 0 15px">涓撹緫EP</p> @@ -836,5 +841,40 @@ h1 { color: #fff; text-align: left; } +.back-button { + z-index: 3; + position: relative; + margin: 24px 0 0 24px; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 50%; + color: #fff; + transition: all 0.2s ease; + + &:hover { + transform: scale(1.1); + background-color: rgba(0, 0, 0, .8); + } +} + +.back-button[data-tooltip]:hover::after { + content: attr(data-tooltip); + position: absolute; + top: 38px; + left: 50%; + transform: translateX(-50%); + background-color: #282828; + color: white; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + z-index: 1000; + pointer-events: none; +} </style> diff --git a/src/components/MusicAlbumView.vue b/src/components/MusicAlbumView.vue index d6c53a6e084628032b6a5fa71d0d89555c8ff8a6..cb0d40df782be82d31cfc2f006e841076883be17 100644 --- a/src/components/MusicAlbumView.vue +++ b/src/components/MusicAlbumView.vue @@ -10,8 +10,7 @@ import pauseButton from "../icon/pauseButton.vue"; import {addSongToPlaylist, modifyPlaylist, removePlaylist, removeSongFromPlaylist} from "../api/playlist"; import {formatTime} from "@/utils/formatTime"; import { loadSongDurations } from '../utils/loadSongDurations'; -import {getPlaylistById, getSongById} from "../api/resolve"; -import {getSongsByPlaylist} from "../api/song"; +import { getRecommendedSongs } from "../api/song"; /* @@ -48,9 +47,8 @@ const edit_title = ref(""); const edit_description = ref(""); const edit_cover_path = ref(""); -// let musicList = ref([]) -// 闅忔満姝屾洸 -const recMusicList = ref([]) +//鎺ㄨ崘姝屾洸 +const recMusicList = ref([]); let musicHoveredIndex = ref(null); let musicClickedIndex = ref(null); @@ -64,7 +62,7 @@ const gradientColor = computed(() => `linear-gradient(to bottom, ${backgroundCol const songDurations = ref(new Map()); watch(() => props.musicList, (newSongs) => { loadSongDurations(newSongs, songDurations); -}, { immediate: true }); +}, { immediate: true ,deep: true}); // 鏀剧缉鏃剁殑缁勪欢澶勭悊 const handleResize = () => { @@ -126,19 +124,6 @@ const debounce = (fn, delay) => { onMounted(() => { - let randomMusicIds =[]; - for (let i = 0; i < 8; i++) { - let num = Math.floor(Math.random()*54+69); - if(randomMusicIds.indexOf(num) === -1) - randomMusicIds.push(num); - else i--; - } - randomMusicIds.forEach((id)=>{ - getSongById({song_id:id}).then(res => { - recMusicList.value.push(res.data.result); - }) - }); - resizeObserver.value = new ResizeObserver(debounce(handleResize, 50)); console.log(resizeObserver.value) nextTick(() => { @@ -225,7 +210,7 @@ const removeAlbum = (albumId) => { const playFromId = (musicId) => { if (musicId === null) { // 浠庡ご寮€濮嬫挱鏀� - musicPlayIndex.value = props.musicList[0].id; + musicPlayIndex = props.musicList[0].id; } else { musicPlayIndex = musicId; } @@ -234,6 +219,13 @@ const playFromId = (musicId) => { musicPauseIndex = null; } +const playRecommendedSongFromId = (musicId) => { + musicPlayIndex = musicId; + const songToPlay = recMusicList.value.find(song => song.id === musicId); + emit('playRecommendedSong', songToPlay) + musicPauseIndex = null; +} + const addToFavorite = (musicId, albumId) => { addSongToPlaylist({ user_id: currentUserId.value, @@ -294,10 +286,6 @@ const quitEdit = () => { const editDesc = document.querySelector(".edit-desc"); editDesc.style.visibility = "hidden"; } -// watch(()=>props.musicList,()=>{ -// musicList.value = props.musicList -// }) - const addRecommendMusic = (musicId) => { console.log(musicId); //TODO:娣诲姞姝屾洸鍒版寚瀹氱殑姝屽崟 @@ -346,6 +334,28 @@ const isCurrentSongInList = computed(() => { return props.musicList.some(song => song.id === musicPlayIndex); }); +// 娣诲姞鑾峰彇鎺ㄨ崘姝屾洸鐨勬柟娉� +const getRecommendations = async () => { + try { + const currentSongIds = props.musicList.map(song => song.id); + + const response = await getRecommendedSongs({ + currentSongIds: currentSongIds, + limit: 3 + }); + + recMusicList.value = response.data.result; + } catch (error) { + console.error("Failed to fetch recommendations:", error); + } +}; + +watch(() => props.musicList, () => { + if (props.musicList.length > 0) { + getRecommendations(); + } +}, { immediate: true }); + </script> <template> @@ -639,7 +649,7 @@ const isCurrentSongInList = computed(() => { @mouseenter="()=>{musicHoveredIndex = music.id;}" @mouseleave="()=>{musicHoveredIndex = null}" @click="musicClickedIndex=music.id" - @dblclick="playFromId(music.id)" + @dblclick="playRecommendedSongFromId(music.id)" :style="{backgroundColor: musicClickedIndex===music.id? '#404040': musicHoveredIndex === music.id ? 'rgba(54,54,54,0.7)' :'rgba(0,0,0,0)', }"> @@ -651,14 +661,14 @@ const isCurrentSongInList = computed(() => { recMusicList.indexOf(music) + 1 }} </div> - <play-button @click="playFromId(music.id)" style="position: absolute;left: 14px;cursor: pointer" + <play-button @click="playRecommendedSongFromId(music.id)" style="position: absolute;left: 14px;cursor: pointer" v-if="(musicHoveredIndex === music.id&&musicPlayIndex!==music.id)||musicPauseIndex===music.id" :style="{color: musicPauseIndex===music.id ? '#1ed660' : 'white'}"/> <pause-button @click="pauseMusic(music.id)" - style="color:#1ed660 ;position: absolute;left: 14px;cursor: pointer" + style="color:#1ed660 ;position: absolute;left: 17px;cursor: pointer" v-if="musicPlayIndex===music.id&&musicHoveredIndex === music.id&&musicPauseIndex!==music.id"/> <img width="17" height="17" alt="" - style="position: absolute;left: 20px;" + style="position: absolute;left: 24px;" v-if="musicPlayIndex===music.id&&musicHoveredIndex !== music.id&&musicPauseIndex!==music.id" src="https://open.spotifycdn.com/cdn/images/equaliser-animated-green.f5eb96f2.gif"> @@ -679,6 +689,7 @@ const isCurrentSongInList = computed(() => { </div> <div class="music-album-info" + @click="emit('openEpisodeView',music.album)" :style="{color:musicHoveredIndex === music.id? 'white' : '#b2b2b2'}"> {{ music.album }} </div> diff --git a/src/components/NotificationToast.vue b/src/components/NotificationToast.vue new file mode 100644 index 0000000000000000000000000000000000000000..806a1cd94ca6b96c7611c06e1e1f30ec74a952f8 --- /dev/null +++ b/src/components/NotificationToast.vue @@ -0,0 +1,135 @@ +<template> + <Transition name="toast"> + <div v-if="show" + class="toast-container" + :class="type"> + <div class="toast-content"> + <div class="toast-message">{{ message }}</div> + </div> + </div> + </Transition> +</template> + +<script setup> +import { ref, onMounted, onUnmounted } from 'vue'; + +const props = defineProps({ + message: { + type: String, + required: true + }, + type: { + type: String, + default: 'info' + }, + duration: { + type: Number, + default: 3000 + } +}); + +const show = ref(false); +let timer = null; + +onMounted(() => { + show.value = true; + if (props.duration > 0) { + timer = setTimeout(() => { + show.value = false; + }, props.duration); + } +}); + +onUnmounted(() => { + if (timer) clearTimeout(timer); +}); +</script> + +<style scoped> +.toast-container { + position: fixed; + top: 8%; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + justify-content: center; + padding: 16px 24px; + color: white; + z-index: 9999; + width: 350px; + border-radius: 8px; + backdrop-filter: blur(8px); +} + +.toast-icon { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-right: 12px; + width: 24px; +} + +.error-icon, .success-icon { + font-size: 20px; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; +} + +.success { + background: rgba(76, 175, 80, 0.95); +} + +.error { + background: rgb(227, 30, 35); +} + +.info { + background: rgba(33, 150, 243, 0.95); +} + +.toast-content { + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + +.toast-message { + font-size: 14px; + line-height: 1.4; + font-weight: 500; + text-align: center; + margin: 0 auto; +} + +.toast-enter-active, +.toast-leave-active { + transition: all 0.3s ease; +} + +.toast-enter-from { + opacity: 0; + transform: translate(-50%, -20px); +} + +.toast-leave-to { + opacity: 0; + transform: translate(-50%, -20px); +} + +.success .toast-message { + color: #E8F5E9; +} + +.error .toast-message { + color: #ffffff; +} + +.toast-container { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} +</style> \ No newline at end of file diff --git a/src/components/NowPlayingView.vue b/src/components/NowPlayingView.vue index 3738ef8ee95062bdb3aa44ebeef1ed7d2d9fb791..26241fee71c9f0a5d145cdf1353bb6d3fe19766e 100644 --- a/src/components/NowPlayingView.vue +++ b/src/components/NowPlayingView.vue @@ -1,36 +1,36 @@ <script setup> -import {defineProps, defineEmits, ref, computed, watch} from 'vue'; +import {defineProps, defineEmits, ref, computed, watch, onMounted} from 'vue'; import {getArtistInfo} from "@/api/artist"; const props = defineProps({ isVisible: Boolean, currentSong: Object, nextSong: Object, + followedArtistIds:Array }); -const isFollowing = ref(false); +const userToken = ref(JSON.parse(sessionStorage.getItem('user-token'))); +const currentUserId = ref(userToken.value.id); + +const artistsInfo = ref([]); const artists = computed(() => { if (!props.currentSong?.artist) return []; return props.currentSong.artist.split('/').map(name => name.trim()); }); -const artistImage = ref([]); const setArtistInfo = async () => { - artistImage.value = []; - - console.log('Artists:', artists.value) + artistsInfo.value = []; + for (const artist of artists.value) { try { const artistInfo = await getArtistInfo(artist); - artistImage.value.push(artistInfo.data.result.avatarUrl); + artistsInfo.value.push(artistInfo.data.result); } catch (error) { console.error('Failed to fetch artist info:', error); } } - - console.log('Artist Images:', artistImage.value) }; watch(artists, () => { @@ -39,16 +39,12 @@ watch(artists, () => { } }, { immediate: true }); -const toggleFollow = () => { - isFollowing.value = !isFollowing.value; - // TODO: 璋冪敤鍚庣API瀹炵幇鍏虫敞/鍙栨秷鍏虫敞 - // followArtist({ - // artistId: artist.value.id, - // isFollow: isFollowing.value - // }); +const toggleFollow = async (index) => { + const artist = artistsInfo.value[index]; + emit('toggleFollow',artist.id,props.followedArtistIds.includes(artistsInfo.value[index].id)); }; -const emit = defineEmits(['playNext', 'toggleQueue', 'enterAuthorDescription']); +const emit = defineEmits(['playNext', 'toggleQueue', 'enterAuthorDescription', 'toggleFollow']); const playNext = () => { emit('playNext'); @@ -61,6 +57,7 @@ const toggleQueue = () => { const enterAuthorDescription = (artistName) => { emit('enterAuthorDescription', artistName); } + </script> <template> @@ -81,16 +78,16 @@ const enterAuthorDescription = (artistName) => { <div v-for="(artist, index) in artists" :key="artist" class="artist-info"> - <img :src="artistImage[index]" + <img :src="artistsInfo[index]?.avatarUrl" class="artist-image" alt="鑹烘湳瀹�" @click="enterAuthorDescription(artist)"/> <div class="artist-details"> <h4>{{ artist }}</h4> <button class="follow-button" - :class="{ 'following': isFollowing }" - @click="toggleFollow"> - {{ isFollowing ? '宸插叧娉�' : '鍏虫敞' }} + :class="{ 'following': props.followedArtistIds.includes(artistsInfo[index]?.id) }" + @click="toggleFollow(index)"> + {{ props.followedArtistIds.includes(artistsInfo[index]?.id) ? '宸插叧娉�' : '鍏虫敞' }} </button> </div> </div> diff --git a/src/components/SearchView.vue b/src/components/SearchView.vue index 5bb929648535659fa6e6720a1afb67815791dea4..cc51928487d7c52523f7922c10f61ead995685fd 100644 --- a/src/components/SearchView.vue +++ b/src/components/SearchView.vue @@ -1,19 +1,108 @@ <script setup> -import {ref} from "vue"; +import {nextTick, onMounted, onUnmounted, ref, watch} from "vue"; import Empty from "./Empty.vue"; +import {formatTime} from "@/utils/formatTime"; +import {ElMessage, ElPopover} from "element-plus"; +import {loadSongDurations} from "@/utils/loadSongDurations"; +import {addSongToPlaylist} from "@/api/playlist"; +import playButton from "../icon/playButton.vue"; +import checkMark from "../icon/checkMark.vue"; +import pauseButton from "../icon/pauseButton.vue"; -const emit = defineEmits(['back']); - -const {songResult, playlistResult} = defineProps({ - songResult: Array, - playlistResult: Array -}) - +const emit = defineEmits(['pauseSong', 'switchSong', 'back', 'switchToArtist']); +const props = defineProps({ + songResult: { + type: Array, + required: true, + }, + playlistResult: { + type: Array, + required: true, + }, + playList: { //鎸囧綋鍓嶆敹钘忕殑姝屽崟鍒楄〃 + type: Array, + required: true, + }, + currentSongId: { + type: Number, + // required: true + }, + isPaused: { + type: Boolean, + } +}); const currentTab = ref('songs') const handleTabClick = (tab) => { - currentTab.value = tab + currentTab.value = tab +} +const songDurations = ref(new Map()); +watch(() => props.musicList, (newSongs) => { + loadSongDurations(newSongs, songDurations); +}, { immediate: true }); + +let musicHoveredIndex = ref(null); +let musicClickedIndex = ref(null); +let musicPlayIndex = ref(null); +let musicPauseIndex = ref(null); + +const playFromId = (musicId) => { + if (musicId === null || musicId === undefined) { + // 浠庡ご寮€濮嬫挱鏀� + musicPlayIndex = props.currentSongId; + } else { + musicPlayIndex = musicId; + } + emit('switchSong', musicPlayIndex, true); + musicPauseIndex = null; +} + +const pauseMusic = (musicId) => { + musicPauseIndex = musicId; + emit('pauseSong'); +} + +const enterArtistDescription = (artistName) => { + emit('switchToArtist', artistName); +} +const addToFavorite = (musicId, albumId) => { + addSongToPlaylist({ + user_id: currentUserId.value, + playlist_id: albumId, + song_id: musicId, + }).then(() => { + ElMessage({ + message: "娣诲姞鑷�: " + props.albumInfo.title, + grouping: true, + type: 'info', + offset: 16, + customClass: "reco-message", + duration: 4000, + }) + }) } + +const popovers = ref([]) +const getPopoverIndex = (popover) => { + if (popover) { + popovers.value.push(popover); + } +} +const closePopover = (e) => { + popovers.value.forEach((item) => { + item.hide(); + }) +} + +onMounted(() => { + musicPlayIndex = props.currentSongId; + musicClickedIndex = props.currentSongId; + musicPauseIndex = props.isPaused ? props.currentSongId : null; +}) + +onUnmounted(() => { + popovers.value = null; +}) </script> <template> @@ -37,20 +126,120 @@ const handleTabClick = (tab) => { >姝屽崟</button> </div> <div class="search-results"> - <ul v-if="currentTab === 'songs'"> - <li v-for="(song, index) in songResult" :key="song.id"> - <div class="song-item"> - <span class="song-index">{{ index + 1 }}</span> - <img :src="song.picPath" class="song-pic pic" alt=""/> - <div class="song-info info"> - <h3 class="song-title">{{ song.title }}</h3> - <p class="song-artist">{{ song.artist }}</p> - <p class="song-album">{{ song.album }}</p> - </div> - </div> - </li> - - </ul> + <div v-if="currentTab === 'songs'"> + <div class="tips"> + <p style="position:absolute; left:25px">#</p> + <p style="position:absolute; left:140px">鏍囬</p> + <p style="margin-left: auto; margin-right:30px">鏀惰棌</p> + </div> + <div class="musicList"> + <div class="music-item" + v-for="music in songResult" + :key="music.id" + :aria-selected="musicClickedIndex === music.id ? 'True':'False'" + @mouseenter="()=>{musicHoveredIndex = music.id;}" + @mouseleave="()=>{musicHoveredIndex = -1}" + @click="musicClickedIndex=music.id" + @dblclick="playFromId(music.id)" + :style="{backgroundColor: musicClickedIndex===music.id? '#404040': + musicHoveredIndex === music.id ? 'rgba(54,54,54,0.7)' :'rgba(0,0,0,0)', + }"> <!--@click浜嬩欢鍐欏湪script涓殑鍑芥暟閲� 鏃犳硶鍙婃椂瑙﹀彂:style涓殑鏍峰紡!!!--> + + <div + :style="{visibility: musicHoveredIndex === music.id||musicPlayIndex === music.id ? 'hidden' : 'visible' }"> + {{ + songResult.indexOf(music) + 1 + }} + </div> + <play-button @click="playFromId(music.id)" style="position: absolute;left: 14px;cursor: pointer" + v-if="(musicHoveredIndex === music.id&&musicPlayIndex!==music.id)||musicPauseIndex===music.id" + :style="{color: musicPauseIndex===music.id ? '#1ed660' : 'white'}"/> + + <pause-button @click="pauseMusic(music.id)" + style="color:#1ed660 ;position: absolute;left: 17px;cursor: pointer" + v-if="musicPlayIndex===music.id&&musicHoveredIndex === music.id&&musicPauseIndex!==music.id"/> + <img width="17" height="17" alt="" + style="position: absolute;left: 24px;" + v-if="musicPlayIndex===music.id&&musicHoveredIndex !== music.id&&musicPauseIndex!==music.id" + src="https://open.spotifycdn.com/cdn/images/equaliser-animated-green.f5eb96f2.gif"> + + <div class="music-detailed-info"> + <img class="music-image" + :src="music.picPath" + alt="姝屾洸鍥剧墖"/> + <div class="music-name-author" style="padding-left: 5px;"> + <p class="music-name" + :style="{color : musicPlayIndex ===music.id? '#1ED660':''}" + :class="[musicPlayIndex === music.id ? 'music-after-click' : '']" + >{{ music.title }} </p> + <p class="music-author" @click="enterArtistDescription(music.artist)" + :style="{color:musicHoveredIndex === music.id? 'white' : '#b2b2b2'}"> + {{ music.artist }}</p> + </div> + </div> + <div class="music-right-info"> + <el-popover + :ref="getPopoverIndex" + class="music-dropdown-options" + popper-class="my-popover" + :width="400" + trigger="click" + :hide-after=0 + + > + <template #reference> + <check-mark class="check-mark" v-tippy="'鍔犲叆姝屽崟'" + :style="{visibility: musicHoveredIndex === music.id ? 'visible' : 'hidden'}"/> + </template> + + <ul @click="closePopover" style="overflow: scroll;max-height: 400px;"> + <div style="padding: 6px 0 6px 10px;font-weight: bold;color:darkgrey;font-size:16px"> + 閫夋嫨姝屽崟鏀惰棌 + </div> + <hr style=" border: 0;padding-top: 1px;background: linear-gradient(to right, transparent, #98989b, transparent);"> + + <li class="album-to-add" @click="addToFavorite(music.id,album.id)" + v-for="album in playList"> + <div style=" + height:40px; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 20px; + font-weight:400" + > + <div style="display: flex; flex-direction: row"> + <img :src="album.picPath" style="height: 40px; width:40px; border-radius: 4px" alt=""/> + <div style=" + margin-left: 10px; + font-size: 18px; + ">{{ album.title }}</div> + </div> + <div style="font-size: 14px; color: #a4a4a4">{{ album.songNum }}棣�</div> + </div> + + </li> + </ul> + </el-popover> + <div style="margin-left: auto;margin-right: 15px; color: #b2b2b2" + :style="{color:musicHoveredIndex === music.id? 'white' : '#b2b2b2'}" + v-show="songDurations.get(music.id) !== undefined"> + {{ formatTime(songDurations.get(music.id)) }} + </div> + <el-popover + :ref="getPopoverIndex" + class="music-dropdown-options" + popper-class="my-popover" + :width="400" + trigger="click" + :hide-after=0 + > + + </el-popover> + </div> + </div> + </div> + </div> <ul v-if="currentTab === 'playlists'"> <li v-for="(playlist, index) in playlistResult" :key="playlist.id"> <div class="playlist-item"> @@ -74,6 +263,7 @@ const handleTabClick = (tab) => { <style scoped> .search-view { padding: 0; + width: 100%; } .back-button { @@ -144,6 +334,119 @@ const handleTabClick = (tab) => { .search-results { list-style-type: none; padding: 0; + width: 100%; +} + +.tips { + z-index: 0; + display: flex; + position: relative; + padding: 5px 8px 5px 8px; + width: 100%; + box-sizing: border-box; + user-select: none; + color: #fff; +} + +.musicList { + border-top: 1px solid #363636; + margin-top: 10px; +} + +/*姣忚闊充箰鐨勬牱寮�*/ +.music-item { + position: relative; + border-radius: 10px; + display: flex; + align-items: center; + padding: 10px 0 10px 25px; + flex-grow: 1; + min-width: 0; + color: #fff; +} + +/*宸︿晶淇℃伅*/ +.music-detailed-info { + display: flex; + flex-direction: row; +} + +.music-image { + display: flex; + align-items: center; + padding-left: 30px; + height: 50px; + width: 50px; /* Adjust as needed */ + border-radius: 10px; +} + +.music-name { + padding-bottom: 5px; + font-size: 18px +} + +.music-name:hover { + cursor: pointer; + text-decoration: underline; +} + +.music-author { + color: #b2b2b2; + font-size: 15px +} + +.music-author:hover { + cursor: pointer; + text-decoration: underline; +} + +/*鍙充晶淇℃伅*/ +.music-right-info { + margin-left: auto; + display: flex; + align-items: center; + flex-direction: row; + color:white; +} + +.check-mark { + width: 20px; + height: auto; + margin-right: 40px; + color: white; + font-weight: bolder; + border-radius: 50%; +} + +.check-mark:hover { + cursor: pointer; +} + +.check-mark:focus { + outline: none; +} + +.album-to-add { + padding: 8px; +} + +.music-more-info { + margin-right: 14px; + font-size: 22px; + transition: width 0.1s ease-in-out; +} + +.music-more-info:focus { + outline: none; + transform: scale(1.05); +} + +.music-more-info:hover { + cursor: pointer; +} + +.music-dropdown-options { + border-radius: 6px; } .info h3 { diff --git a/src/router/index.js b/src/router/index.js index 5a74422b6ae1afa256e79a35e76170b75a01c38a..3bf55af34e83c3124058c375e32a710bd46a766d 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -49,4 +49,23 @@ const router = createRouter({ ] }) +router.beforeEach((to, _, next) => { + const token = sessionStorage.getItem('token'); + console.log(token) + + if (to.meta.title) { + document.title = to.meta.title + } + + if ( token == null) { + if(to.path !== '/login'){ + next('/login') + }else { + next() + } + }else { + next() + } +}) + export {router} diff --git a/src/views/HomePage.vue b/src/views/HomePage.vue index edc8adba19073863e5eeeaf55c2f310ffd217257..7fdcdd88c0fff5b6b35821a61d1ca3d3006be4c5 100644 --- a/src/views/HomePage.vue +++ b/src/views/HomePage.vue @@ -1,7 +1,7 @@ /* eslint-disable */ <script setup> // Vue Basics -import {computed, onMounted, ref, watch} from "vue" +import {computed, onMounted, ref, watch, onUnmounted} from "vue" // Assets import defaultBg from '../assets/pictures/Eason.png' @@ -27,7 +27,8 @@ import {useTheme} from "../store/theme"; import {parseLrc} from "../utils/parseLyrics" import {updateBackground} from "../utils/getBackgroundColor"; import { formatTime } from '../utils/formatTime'; -import {getPlaylistById} from "../api/resolve"; +import {getArtistById, getPlaylistById, getUserById} from "../api/resolve"; +import {userFollowArtist} from "@/api/user"; /* @@ -421,8 +422,9 @@ const switchToPlaylist = (playlist, songId) => { getSongsByPlaylist({ playlist_id: currentPlaylistId.value, }).then((res) => { - songs.value = res.data.result; - displayingSongs.value = res.data.result; + const songsList = res.data.result; + songs.value = [...songsList]; + displayingSongs.value = [...songsList]; currentSongId.value = songId; for (let i = 0; i < songs.value.length; i++) { if (songs.value[i].id === songId) { @@ -439,6 +441,30 @@ const switchToPlaylist = (playlist, songId) => { }); } +const handleRecommendedSong = (songToPlay) => { + const songExists = songs.value.some(song => song.id === songToPlay.id); + if (songExists) { + const songIndex = songs.value.findIndex(song => song.id === songToPlay.id); + songs.value.splice(songIndex, 1) + } + songs.value.unshift(songToPlay); + currentSongIndex.value = 0; + currentSongId.value = songToPlay.id; + if (song) { + controlIcons.forEach(controlIcon => { + controlIcon.src = PLAY; + }); + song.src = songToPlay.filePath; + parseLrc(songToPlay.lyricsPath).then(res => { + lyrics.value = res; + }); + song.load(); + song.play(); + theme.change(songToPlay.picPath); + isPaused.value = false; + } +}; + /* PLAYLISTS @@ -534,31 +560,59 @@ function receiveDataFromHome() { */ const midComponents = ref(0); const currentArtist = ref(null); -const backStack = ref([]); +const navigationHistory = ref([]); + +const setMidComponents = (index, props = null) => { + console.log("from" + midComponents.value + " to " + index) + navigationHistory.value.push({ + index: midComponents.value, + props: { + artistName: currentArtist?.value, + albumInfo: displayingPlaylist?.value, + musicList: displayingSongs?.value, + playList: playlists?.value, + songResult: songResult?.value, + playlistResult: playlistResult?.value, + episodeInfo: displayingEpisode?.value, + playFromLeftBar: playFromLeftBarAlbum?.value, + playlistInfo: currentPlaylist?.value, + } + }); -const setMidComponents = (val, prop = null, isBack = false) => { - console.log("from" + midComponents.value + " to " + val) - if (val !== midComponents.value && !isBack) { - backStack.value.push(midComponents.value); - } - - midComponents.value = val; - - - if (val === 5) { - currentArtist.value = prop; - } else if (val === 6) { - displayingMusicId.value = prop; + midComponents.value = index; + + switch(index) { + case 5: // ArtistView + if (props) { + currentArtist.value = props; + } + break; + case 6: + if (props) { + displayingMusicId.value = props; + } + break; } }; const goBack = () => { - if (backStack.value.length > 0) { - const lastIndex = backStack.value.pop(); - setMidComponents(lastIndex, currentArtist.value, true); - } else { - setMidComponents(0, null); - } + if (navigationHistory.value.length > 0) { + const previousState = navigationHistory.value.pop(); + midComponents.value = previousState.index; + if (previousState.props) { + currentArtist.value = previousState.props.artistName; + displayingPlaylist.value = previousState.props.albumInfo; + displayingSongs.value = previousState.props.musicList; + playlists.value = previousState.props.playList; + songResult.value = previousState.props.songResult; + playlistResult.value = previousState.props.playlistResult; + displayingEpisode.value = previousState.props.episodeInfo; + playFromLeftBarAlbum.value = previousState.props.playFromLeftBar; + currentPlaylist.value = previousState.props.playlistInfo; + } + } else { + midComponents.value = 0; + } }; /* @@ -597,7 +651,39 @@ const playNextSong = () => { switchSongs(1); }; +/* + followInfo + */ +const followedArtistInfo = ref([]); +const toggleFollow = async (artistId, isFollowed) => { + try { + await userFollowArtist({ + user_id: currentUserId.value, + artist_id: artistId, + isFollowed: isFollowed + }); + + if (!isFollowed) { + const artistResponse = await getArtistById(artistId); + followedArtistInfo.value.push(artistResponse.data.result); + } else { + const artistIndex = followedArtistInfo.value.findIndex(artist => artist.id === artistId); + if (artistIndex > -1) { + followedArtistInfo.value.splice(artistIndex, 1); + } + } + } catch (error) { + console.error("Failed to update artist follow status:", error); + } +}; + onMounted(() => { + getUserById({ userId: currentUserId.value }).then(async res => { + const artistPromises = res.data.result.followedArtistIds.map(id => + getArtistById(id).then(res => res.data.result) + ); + followedArtistInfo.value = await Promise.all(artistPromises); + }); /* DOMS & EVENTS */ @@ -686,7 +772,6 @@ const updateSongs = (newSongs) => { songs.value = newSongs; displayingSongs.value = newSongs; }; - </script> <template> @@ -717,6 +802,7 @@ const updateSongs = (newSongs) => { :playFromLeftBar="playFromLeftBarAlbum" :is-paused="isPaused" @switchSongs="switchToPlaylist" + @playRecommendedSong="handleRecommendedSong" @switchToArtist="(name) => setMidComponents(5, name)" @pauseSong="pauseCurrentSong" @back="goBack" @@ -731,7 +817,14 @@ const updateSongs = (newSongs) => { </el-container> <el-container v-if="midComponents === 3" class="playlist-container" style="overflow: auto; height: 730px ;border-radius: 12px"> - <SearchView :songResult="songResult" :playlistResult="playlistResult" + <SearchView :songResult="songResult" + :playlistResult="playlistResult" + :play-list="playlists" + :current-song-id="currentSongId" + :is-paused="isPaused" + @switchSong="switchToSong" + @pauseSong="pauseCurrentSong" + @switchToArtist="(name) => setMidComponents(5, name)" @back="goBack"/> </el-container> <div v-if="midComponents === 4" class="playlist-container" @@ -754,10 +847,12 @@ const updateSongs = (newSongs) => { <ArtistView :artist-name="currentArtist" :is-paused="isPaused" :current-song-id="currentSongId" + :is-followed="followedArtistInfo.some(artist => artist.name === currentArtist)" @playSong="playArtistSong" @pauseSong="pauseCurrentSong" @back="goBack" - @updateSongs="updateSongs"/> + @updateSongs="updateSongs" + @toggleFollow="toggleFollow"/> </div> </div> <div v-if="showRightContent" class="right-content"> @@ -821,9 +916,11 @@ const updateSongs = (newSongs) => { :is-visible="showNowPlaying" :current-song="songs[currentSongIndex]" :next-song="getNextSong()" + :followed-artist-ids="followedArtistInfo.map(artist => artist.id)" @play-next="playNextSong" @toggle-queue="toggleQueue" @enter-author-description="(name) => setMidComponents(5, name)" + @toggleFollow="toggleFollow" /> </div> </div> diff --git a/src/views/LoginPage.vue b/src/views/LoginPage.vue index f2d54a9f08a3c1387fbc403917c51af799c02367..b85882d2f84fc85b1ebbb7d695b04ce404dfd4a2 100644 --- a/src/views/LoginPage.vue +++ b/src/views/LoginPage.vue @@ -1,154 +1,407 @@ <script setup> -import {onMounted, ref} from "vue"; -import {userInfo, userLogin, userRegister} from "../api/user.js"; +import {computed, onMounted, ref, onUnmounted} from "vue"; +import {userInfo, userLogin, userRegister, userReset, userSendCaptcha} from "../api/user.js"; import {useTheme} from "../store/theme"; import {router} from "../router"; +import {ElMessage} from "element-plus"; +import {md5} from "js-md5"; +import NotificationToast from '../components/NotificationToast.vue'; const theme = useTheme() -const login_email = ref("cossky@outlook.com") -const login_password = ref("1145141919810") -const login_prompt = ref("") +const loginPhone = ref(''); +const registerPhone = ref(''); +const resetPasswordPhone = ref(''); +const loginPassword = ref(''); +const registerPassword = ref(''); +const registerPasswordConfirm = ref(''); +const resetPassword = ref(''); +const resetPasswordConfirm = ref('');//鍐嶆杈撳叆瀵嗙爜 +const registerCaptcha = ref(''); +const resetPasswordCaptcha = ref(''); +const name = ref(''); +const reset = ref(false); +const sendingCaptcha = ref(false); -const register_name = ref("CosSky") -const register_email = ref("cossky@outlook.com") -const register_password = ref("1145141919810") -const register_prompt = ref("") +// 鐢佃瘽鍙风爜鐨勮鍒� +const chinaMobileRegex = /^(?:(?:\+|00)86)?1(?:3\d|4[5-79]|5[0-35-9]|6[5-7]|7[0-8]|8\d|9[01256789])\d{8}$/ + +const toasts = ref([]); +let toastId = 0; + +const showToast = (message, type = 'info') => { + const id = toastId++; + toasts.value.push({ + id, + message, + type + }); + + setTimeout(() => { + toasts.value = toasts.value.filter(toast => toast.id !== id); + }, 3000); +}; + +// 淇敼琛ㄥ崟楠岃瘉鍜屾彁绀� +const validatePhone = (phone) => { + if (!phone) { + showToast('璇疯緭鍏ユ墜鏈哄彿鐮�', 'error'); + return false; + } + if (!chinaMobileRegex.test(phone)) { + showToast('璇疯緭鍏ユ纭殑鎵嬫満鍙风爜鏍煎紡', 'error'); + return false; + } + return true; +}; + +onMounted(() => { + theme.reset(); + + let switchCtn = document.querySelector("#switch-cnt"); + let switchC1 = document.querySelector("#switch-c1"); + let switchC2 = document.querySelector("#switch-c2"); + let switchCircle = document.querySelectorAll(".switch_circle"); + let switchBtn = document.querySelectorAll(".switch-btn"); + let aContainer = document.querySelector("#a-container"); + let bContainer = document.querySelector("#b-container"); + let allButtons = document.querySelectorAll(".submit"); + + const initializeLoginForm = () => { + // 璁剧疆鍒濆鐘舵€佷负鐧诲綍琛ㄥ崟 + switchCtn.classList.add("is-txr"); + switchCircle[0].classList.add("is-txr"); + switchCircle[1].classList.add("is-txr"); + + switchC1.classList.add("is-hidden"); + switchC2.classList.remove("is-hidden"); + aContainer.classList.add("is-txl"); + bContainer.classList.add("is-txl"); + bContainer.classList.add("is-z"); + } + + let getButtons = (e) => e.preventDefault() + let changeForm = () => { + // 淇敼绫诲悕 + switchCtn.classList.add("is-gx"); + setTimeout(function () { + switchCtn.classList.remove("is-gx"); + }, 1500) + switchCtn.classList.toggle("is-txr"); + switchCircle[0].classList.toggle("is-txr"); + switchCircle[1].classList.toggle("is-txr"); + + switchC1.classList.toggle("is-hidden"); + switchC2.classList.toggle("is-hidden"); + aContainer.classList.toggle("is-txl"); + bContainer.classList.toggle("is-txl"); + bContainer.classList.toggle("is-z"); + } + + // 鐐瑰嚮鍒囨崲 + const setupEventListeners = () => { + allButtons.forEach(button => { + button.addEventListener("click", getButtons); + }); + switchBtn.forEach(btn => { + btn.addEventListener("click", changeForm); + }); + } + + initializeLoginForm(); + setupEventListeners(); + + onUnmounted(() => { + allButtons.forEach(button => { + button.removeEventListener("click", getButtons); + }); + switchBtn.forEach(btn => { + btn.removeEventListener("click", changeForm); + }); + }); +}) function handleLogin() { - userLogin({ - email: login_email.value, - password: login_password.value, - }).then(res => { - if (res.data.code === '000' || res.data.code === '200') { - sessionStorage.setItem('token', res.data.result) - userInfo().then(res => { - sessionStorage.setItem('user-token', JSON.stringify(res.data.result)) - console.log("Storing session: ", res.data.result); - router.push({path: "/home"}) - // document.documentElement.requestFullscreen(); - }) - } else if (res.data.code === '400') { + if (!validatePhone(loginPhone.value)) return; - } - }).catch(() => { - login_prompt.value = "Wrong email or password! :("; - }) + if (!loginPassword.value) { + showToast('璇疯緭鍏ュ瘑鐮�', 'error'); + return; + } + + const hashedPassword = md5.create().update(loginPassword.value); + userLogin({ + phone: loginPhone.value, + password: hashedPassword.hex() + }).then(res => { + if (res.data.code === '000' || res.data.code === '200') { + showToast('鐧诲綍鎴愬姛', 'success'); + const token = res.data.result; + sessionStorage.setItem('token', token); + userInfo().then(res => { + sessionStorage.setItem('user-token', JSON.stringify(res.data.result)); + router.push({path: "/home"}); + }); + } else if (res.data.code === '400') { + showToast(res.data.msg || '鎵嬫満鍙锋垨瀵嗙爜閿欒', 'error'); + loginPassword.value = ''; + } + }).catch(() => { + showToast('鐧诲綍澶辫触锛岃绋嶅悗閲嶈瘯', 'error'); + }); } + +// 娣诲姞琛ㄥ崟楠岃瘉鏂规硶 +const validateRegisterForm = () => { + if (!name.value) { + showToast('璇疯緭鍏ョ敤鎴峰悕', 'error'); + return false; + } + if (!validatePhone(registerPhone.value)) return false; + if (!registerCaptcha.value) { + showToast('璇疯緭鍏ラ獙璇佺爜', 'error'); + return false; + } + if (!registerPassword.value) { + showToast('璇疯緭鍏ュ瘑鐮�', 'error'); + return false; + } + if (!registerPasswordConfirm.value) { + showToast('璇风‘璁ゅ瘑鐮�', 'error'); + return false; + } + if (registerPassword.value !== registerPasswordConfirm.value) { + showToast('涓ゆ杈撳叆鐨勫瘑鐮佷笉涓€鑷�', 'error'); + return false; + } + return true; +}; + +const validateResetForm = () => { + if (!validatePhone(resetPasswordPhone.value)) return false; + if (!resetPasswordCaptcha.value) { + showToast('璇疯緭鍏ラ獙璇佺爜', 'error'); + return false; + } + if (!resetPassword.value) { + showToast('璇疯緭鍏ユ柊瀵嗙爜', 'error'); + return false; + } + if (!resetPasswordConfirm.value) { + showToast('璇风‘璁ゆ柊瀵嗙爜', 'error'); + return false; + } + if (resetPassword.value !== resetPasswordConfirm.value) { + showToast('涓ゆ杈撳叆鐨勫瘑鐮佷笉涓€鑷�', 'error'); + return false; + } + return true; +}; + +const validateSendCaptcha = (phone) => { + if (!phone) { + showToast('璇疯緭鍏ユ墜鏈哄彿鐮�', 'error'); + return false; + } + if (!chinaMobileRegex.test(phone)) { + showToast('璇疯緭鍏ユ纭殑鎵嬫満鍙风爜鏍煎紡', 'error'); + return false; + } + return true; +}; + function handleRegister() { - userRegister({ - username: register_name.value, - email: register_email.value, - password: register_password.value, - }).then(res => { - if (res.data.code === '000' || res.data.code === '200') { - let userForms = document.getElementById('user_options-forms') - userForms.classList.remove('bounceLeft') - userForms.classList.add('bounceRight') - } else if (res.data.code === '400') { - - } - }).catch(() => { - register_prompt.value = "Email already taken! :("; - }) + if (!validateRegisterForm()) return; + + const hashedPassword = md5.create().update(registerPassword.value); + userRegister({ + name: name.value, + phone: registerPhone.value, + password: hashedPassword.hex(), + captcha: registerCaptcha.value + }).then(res => { + if (res.data.code === '000' || res.data.code === '200') { + showToast('娉ㄥ唽鎴愬姛', 'success'); + location.reload(); + } else if (res.data.code === '400') { + showToast(res.data.msg || '娉ㄥ唽澶辫触锛岃閲嶈瘯', 'error'); + } + }).catch(() => { + showToast('娉ㄥ唽澶辫触锛岃绋嶅悗閲嶈瘯', 'error'); + }); } -onMounted(() => { - theme.reset(); - - /** - * Variables - */ - const signupButton = document.getElementById('signup-button'), - loginButton = document.getElementById('login-button'), - userForms = document.getElementById('user_options-forms') - - /** - * Add event listener to the "Sign Up" button - */ - signupButton.addEventListener('click', () => { - userForms.classList.remove('bounceRight') - userForms.classList.add('bounceLeft') - register_prompt.value = ""; - }, false) - - /** - * Add event listener to the "Login" button - */ - loginButton.addEventListener('click', () => { - userForms.classList.remove('bounceLeft') - userForms.classList.add('bounceRight') - login_prompt.value = ""; - }, false) -}) +function handleResetPassword() { + if (!validateResetForm()) return; + + const hashedPassword = md5.create().update(resetPassword.value); + userReset({ + phone: resetPasswordPhone.value, + password: hashedPassword.hex(), + captcha: resetPasswordCaptcha.value + }).then(res => { + if (res.data.code === '000' || res.data.code === '200') { + showToast('瀵嗙爜閲嶇疆鎴愬姛', 'success'); + location.reload(); + } else if (res.data.code === '400') { + showToast(res.data.msg || '閲嶇疆澶辫触锛岃閲嶈瘯', 'error'); + } + }).catch(() => { + showToast('閲嶇疆澶辫触锛岃绋嶅悗閲嶈瘯', 'error'); + }); +} + +function handleSendCaptcha(phone) { + if (!validateSendCaptcha(phone)) return; + if (sendingCaptcha.value) { + showToast('璇风瓑寰呭€掕鏃剁粨鏉熷悗鍐嶆鍙戦€�', 'info'); + return; + } + + userSendCaptcha({ + phone: phone + }).then(res => { + if (res.data.code === '000' || res.data.code === '200') { + showToast('楠岃瘉鐮佸彂閫佹垚鍔�', 'success'); + startCountdown(); + } else if (res.data.code === '400') { + showToast(res.data.msg || '鍙戦€佸け璐ワ紝璇烽噸璇�', 'error'); + } + }).catch(() => { + showToast('鍙戦€佸け璐ワ紝璇风◢鍚庨噸璇�', 'error'); + }); +} + +function startCountdown() { + const sendCaptchaButton = document.getElementById('sendCaptcha'); + sendingCaptcha.value = true; + let seconds = 60; + const countdown = setInterval(() => { + seconds--; + sendCaptchaButton.textContent = `${seconds}s RETRY`; + if (seconds <= 0) { + clearInterval(countdown); + sendCaptchaButton.textContent = 'SEND CAPTCHA'; + sendingCaptcha.value = false; + } + }, 1000); +} </script> <template> <body> <video autoplay muted loop id="video-background"> - <source src="../assets/videos/1.mp4" type="video/mp4"> + <source src="../assets/videos/3.mp4" type="video/mp4"> Your browser does not support the video tag. </video> <img class="logo" src="../assets/pictures/logos/logo1.png" alt=""> - <section class="user"> - <div class="user_options-container"> - <div class="user_options-text"> - <div class="user_options-unregistered"> - <h2 class="user_registered-title">娆㈣繋鍥炴潵锛岄煶涔愯揪浜猴紒</h2> - <p class="user_registered-text">杈撳叆浣犵殑璐﹀彿鍜屽瘑鐮侊紝缁х画璺熼殢鑺傛媿鍓嶈銆傝繕璁板緱浣犱笂娆″湪鈥滄垜鐨勬瓕鍗曗€濋噷鏀惰棌浜嗗摢浜涙瓕鏇插悧锛熷揩鏉ュ拰鎴戜滑涓€璧烽噸娓╅偅浜涚編濡欑殑鏃嬪緥鍚э紒濡傛灉杩樻病鏈夎处鎴凤紝鍙互鐐瑰嚮涓嬫柟娉ㄥ唽鍝</p> - <button class="user_unregistered-signup" id="signup-button">Sign up</button> - </div> - - <div class="user_options-registered"> - <h2 class="user_unregistered-title">浣犲拰鎴愪负闊充箰杈句汉鍙湁涓€姝ヤ箣閬ワ紒</h2> - <p class="user_unregistered-text">娉ㄥ唽涓€涓处鎴凤紝瑙i攣鏃犻檺鏇插簱銆佷釜鎬у寲姝屽崟鍜屾洿澶氱濂囧姛鑳斤紒濉啓浠ヤ笅淇℃伅锛岄┈涓婂紑濮嬩綘鐨勯煶涔愪箣鏃咃紒濡傛灉宸叉湁璐︽埛锛岀偣鍑讳笅鏂圭櫥褰曞惂锛�</p> - <button class="user_registered-login" id="login-button">Login</button> - </div> - </div> - - <div class="user_options-forms" id="user_options-forms"> - <div class="user_forms-login"> - <h2 class="forms_title">Login</h2> - <form class="forms_form"> - <fieldset class="forms_fieldset"> - <div class="forms_field"> - <input type="email" v-model="login_email" placeholder="Email" class="forms_field-input" required autofocus /> - </div> - <div class="forms_field"> - <input type="password" v-model="login_password" placeholder="Password" class="forms_field-input" required /> - </div> - </fieldset> - <p style="font-family: 'Comic Sans MS', serif; color: red">{{ login_prompt }}</p> - <div class="forms_buttons"> - <button type="button" class="forms_buttons-forgot">蹇樿瀵嗙爜?</button> - <input @click="handleLogin" type="submit" value="Log In" class="forms_buttons-action"> - </div> - </form> - </div> - <div class="user_forms-signup"> - <h2 class="forms_title">Sign Up</h2> - <form class="forms_form"> - <fieldset class="forms_fieldset"> - <div class="forms_field"> - <input type="text" v-model="register_name" placeholder="Username" class="forms_field-input" required /> - </div> - <div class="forms_field"> - <input type="email" v-model="register_email" placeholder="Email" class="forms_field-input" required /> - </div> - <div class="forms_field"> - <input type="password" v-model="register_password" placeholder="Password" class="forms_field-input" required /> - </div> - </fieldset> - <p style="font-family: 'Comic Sans MS', serif; color: red">{{ register_prompt }}</p> - <div class="forms_buttons"> - <input @click="handleRegister" type="submit" value="Sign up" class="forms_buttons-action"> - </div> - </form> - </div> - </div> - </div> - </section> + <div class="shell"> + <div v-if="!reset" class="container a-container" id="a-container"> + <div class="toast-list"> + <NotificationToast + v-for="toast in toasts" + :key="toast.id" + :message="toast.message" + :type="toast.type" + /> + </div> + <form action="" method="" class="form" id="a-form"> + <h2 class="form_title title">鍒涘缓璐﹀彿</h2> + <input type="text" class="form_input" + placeholder="Name" v-model="name"> + <input type="text" class="form_input" + placeholder="Phone" v-model="registerPhone"> + <input type="text" class="form_input" + placeholder="Captcha" v-model="registerCaptcha"> + <input type="password" class="form_input" + placeholder="Password" v-model="registerPassword"> + <input type="password" class="form_input" :class = "{ 'error': registerPassword !== registerPasswordConfirm}" + placeholder="Confirm Password" v-model="registerPasswordConfirm"> + <div style="display:flex; justify-content: space-between"> + <button style="margin-right: 10px" class="form_button button submit" @click="handleRegister"> + SIGN UP + </button> + <button id="sendCaptcha" style="margin-left: 10px" + class="form_button button submit" + @click="handleSendCaptcha(registerPhone)"> + SEND CAPTCHA + </button> + </div> + + </form> + </div> + + <div v-if="reset" class="container a-container" id="a-container"> + <div class="toast-list"> + <NotificationToast + v-for="toast in toasts" + :key="toast.id" + :message="toast.message" + :type="toast.type" + /> + </div> + <form action="" method="" class="form" id="a-form"> + <h2 class="form_title title">閲嶇疆瀵嗙爜</h2> + <input type="text" class="form_input" + placeholder="Phone" v-model="resetPasswordPhone"> + <input type="text" class="form_input" + placeholder="Captcha" v-model="resetPasswordCaptcha"> + <input type="password" class="form_input" + placeholder="Password" v-model="resetPassword"> + <input type="password" class="form_input" :class = "{ 'error': resetPassword !== resetPasswordConfirm}" + placeholder="Confirm Password" v-model="resetPasswordConfirm"> + <div style="display: flex; justify-content: space-between "> + <button style="margin-right: 10px" class="form_button button submit" @click="handleResetPassword"> + RESET + </button> + <button id="sendCaptcha" style="margin-left: 10px" + class="form_button button submit" + @click="handleSendCaptcha(registerPhone)"> + SEND CAPTCHA + </button> + </div> + </form> + </div> + + <div class="container b-container" id="b-container"> + <div class="toast-list"> + <NotificationToast + v-for="toast in toasts" + :key="toast.id" + :message="toast.message" + :type="toast.type" + /> + </div> + <form action="" method="" class="form" id="b-form"> + <h2 class="form_title title">鐧诲綍璐﹀彿</h2> + <input type="text" class="form_input" + placeholder="Phone" v-model="loginPhone"> + <input type="password" class="form_input" + placeholder="Password" v-model="loginPassword"> + <button class="switch_button button switch-btn" @click="() => {reset = true}">FORGET PASSWORD?</button > + <button class="form_button button submit" @click="handleLogin"> + SIGN IN + </button> + </form> + </div> + + <div class="switch" id="switch-cnt"> + <div class="switch_circle"></div> + <div class="switch_circle switch_circle-t"></div> + <div class="switch_container" id="switch-c1"> + <h2 class="switch_title title" style="letter-spacing: 0;">Welcome Back锛�</h2> + <p class="switch_description description">宸茬粡鏈夎处鍙蜂簡鍢涳紝鍘荤櫥褰曡处鍙风户缁窡闅忚妭鎷嶅墠琛屽惂锛侊紒锛�</p > + <button class="switch_button button switch-btn" @click="reset = false">SIGN IN</button> + </div> + + <div class="switch_container is-hidden" id="switch-c2"> + <h2 class="switch_title title" style="letter-spacing: 0;">Hello Friend锛�</h2> + <p class="switch_description description">鍘绘敞鍐屼竴涓处鍙凤紝鎴愪负灏婅吹鐨勭矇涓濅細鍛橈紝璁╂垜浠笍鍏ュ濡欑殑鏃呴€旓紒</p > + <button class="switch_button button switch-btn">SIGN UP</button> + </div> + </div> + </div> </body> </template> @@ -165,352 +418,314 @@ onMounted(() => { box-sizing: border-box; padding: 0; margin: 0; + user-select: none; } body { - margin: 0; - padding: 0; - height: 100%; - font-family: "Nunito", sans-serif; - display: flex; - align-items: center; - justify-content: center; - background-image: url("../assets/videos/1.mp4"); - background-repeat: no-repeat; - background-size: cover; + width: 100%; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + font-size: 12px; + color: #a0a5a8; } -.logo { - position: absolute; - top: -10px; - left: -10px; - width: 150px; - height: 150px; +.shell { + position: relative; + width: 1000px; + min-width: 1000px; + min-height: 600px; + height: 600px; + padding: 25px; + background-color: #ecf0f3; + box-shadow: 10px 10px 10px #d1d9e6, -10px -10px 10px #f9f9f9; + border-radius: 12px; + overflow: hidden; } -#video-background { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - object-fit: cover; /* 纭繚瑙嗛濉厖鏁翠釜瑙嗗彛 */ - z-index: -1; /* 灏嗚棰戠疆浜庡唴瀹瑰悗闈� */ +/* 璁剧疆鍝嶅簲寮� */ +@media (max-width: 1200px) { + .shell { + transform: scale(0.7); + } } -button { - background-color: transparent; - padding: 0; - border: 0; - outline: 0; - cursor: pointer; +@media (max-width: 1000px) { + .shell { + transform: scale(0.6); + } } -input { - background-color: transparent; - padding: 0; - border: 0; - outline: 0; -} -input[type=submit] { - cursor: pointer; -} -input::-moz-placeholder { - font-size: 0.85rem; - font-family: "Montserrat", sans-serif; - font-weight: 300; - letter-spacing: 0.1rem; - color: #ccc; -} -input:-ms-input-placeholder { - font-size: 0.85rem; - font-family: "Montserrat", sans-serif; - font-weight: 300; - letter-spacing: 0.1rem; - color: #ccc; -} -input::placeholder { - font-size: 0.85rem; - font-family: "Montserrat", sans-serif; - font-weight: 300; - letter-spacing: 0.1rem; - color: #ccc; +@media (max-width: 800px) { + .shell { + transform: scale(0.5); + } } -/** - * * Bounce to the left side - * */ -@-webkit-keyframes bounceLeft { - 0% { - transform: translate3d(100%, -50%, 0); - } - 50% { - transform: translate3d(-30px, -50%, 0); - } - 100% { - transform: translate3d(0, -50%, 0); - } -} -@keyframes bounceLeft { - 0% { - transform: translate3d(100%, -50%, 0); - } - 50% { - transform: translate3d(-30px, -50%, 0); - } - 100% { - transform: translate3d(0, -50%, 0); - } +@media (max-width: 600px) { + .shell { + transform: scale(0.4); + } } -/** - * * Bounce to the left side - * */ -@-webkit-keyframes bounceRight { - 0% { - transform: translate3d(0, -50%, 0); - } - 50% { - transform: translate3d(calc(100% + 30px), -50%, 0); - } - 100% { - transform: translate3d(100%, -50%, 0); - } -} -@keyframes bounceRight { - 0% { - transform: translate3d(0, -50%, 0); - } - 50% { - transform: translate3d(calc(100% + 30px), -50%, 0); - } - 100% { - transform: translate3d(100%, -50%, 0); - } + +.container { + display: flex; + justify-content: center; + align-items: center; + position: absolute; + top: 0; + width: 600px; + height: 100%; + padding: 25px; + background-color: #ecf0f3; + transition: 1.25s; } -/** - * * Show Sign Up form - * */ -@-webkit-keyframes showSignUp { - 100% { - opacity: 1; - visibility: visible; - transform: translate3d(0, 0, 0); - } -} -@keyframes showSignUp { - 100% { - opacity: 1; - visibility: visible; - transform: translate3d(0, 0, 0); - } + +.form { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + width: 100%; + height: 100%; } -/** - * * Page background - * */ -.user { - display: flex; - justify-content: center; - align-items: center; - width: 100%; - height: 100vh; - background-size: cover; + +.iconfont { + margin: 0 5px; + border: rgba(0, 0, 0, 0.5) 2px solid; + border-radius: 50%; + font-size: 25px; + padding: 3px; + opacity: 0.5; + transition: 0.1s; } -.user_options-container { - position: relative; - width: 80%; + +.iconfont:hover { + opacity: 1; + transition: 0.15s; + cursor: pointer; } -.user_options-text { - display: flex; - justify-content: space-between; - width: 100%; - background-color: rgba(34, 34, 34, 0.85); - border-radius: 3px; + +.form_input { + width: 350px; + height: 40px; + margin: 4px 0; + padding-left: 25px; + font-size: 13px; + color: black; + letter-spacing: 0.15px; + border: none; + outline: none; + background-color: #ecf0f3; + transition: 0.25s ease; + border-radius: 8px; + box-shadow: inset 2px 2px 4px #d1d9e6, inset -2px -2px 4px #f9f9f9; } -/** - * * Registered and Unregistered user box and text - * */ -.user_options-registered, -.user_options-unregistered { - width: 50%; - padding: 75px 45px; - color: #fff; - font-weight: 300; +.form_input:focus { + box-shadow: inset 4px 4px 4px #d1d9e6, inset -4px -4px 4px #f9f9f9; } -.user_registered-title, -.user_unregistered-title { - margin-bottom: 15px; - font-size: 1.66rem; - line-height: 1em; +.error { + color: red; /* 閿欒杈规棰滆壊 */ } -.user_unregistered-text, -.user_registered-text { - font-size: 0.83rem; - line-height: 1.4em; +.form_span { + margin-top: 30px; + margin-bottom: 12px; } -.user_registered-login, -.user_unregistered-signup { - margin-top: 30px; - border: 1px solid #ccc; - border-radius: 3px; - padding: 10px 30px; - color: #fff; - text-transform: uppercase; - line-height: 1em; - letter-spacing: 0.2rem; - transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; +.form_link { + color: #181818; + font-size: 15px; + margin-top: 25px; + border-bottom: 1px solid #a0a5a8; + line-height: 2; } -.user_registered-login:hover, -.user_unregistered-signup:hover { - color: rgba(34, 34, 34, 0.85); - background-color: #ccc; + +.title { + font-size: 34px; + font-weight: 700; + line-height: 3; + color: #181818; + letter-spacing: 10px; } -/** - * * Login and signup forms - * */ -.user_options-forms { - position: absolute; - top: 50%; - left: 30px; - width: calc(50% - 30px); - min-height: 420px; - background-color: #fff; - border-radius: 3px; - box-shadow: 2px 0 15px rgba(0, 0, 0, 0.25); - overflow: hidden; - transform: translate3d(100%, -50%, 0); - transition: transform 0.4s ease-in-out; -} -.user_options-forms .user_forms-login { - transition: opacity 0.4s ease-in-out, visibility 0.4s ease-in-out; -} -.user_options-forms .forms_title { - margin-bottom: 45px; - font-size: 1.5rem; - font-weight: 500; - line-height: 1em; - text-transform: uppercase; - color: #e8716d; - letter-spacing: 0.1rem; -} -.user_options-forms .forms_field:not(:last-of-type) { - margin-bottom: 20px; -} -.user_options-forms .forms_field-input { - width: 100%; - border-bottom: 1px solid #ccc; - padding: 6px 20px 6px 6px; - font-family: "Montserrat", sans-serif; - font-size: 1rem; - font-weight: 300; - color: gray; - letter-spacing: 0.1rem; - transition: border-color 0.2s ease-in-out; -} -.user_options-forms .forms_field-input:focus { - border-color: gray; -} -.user_options-forms .forms_buttons { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 35px; -} -.user_options-forms .forms_buttons-forgot { - font-family: "Montserrat", sans-serif; - letter-spacing: 0.1rem; - color: #ccc; - text-decoration: underline; - transition: color 0.2s ease-in-out; -} -.user_options-forms .forms_buttons-forgot:hover { - color: #b3b3b3; -} -.user_options-forms .forms_buttons-action { - background-color: #e8716d; - border-radius: 3px; - padding: 10px 35px; - font-size: 1rem; - font-family: "Montserrat", sans-serif; - font-weight: 300; - color: #fff; - text-transform: uppercase; - letter-spacing: 0.1rem; - transition: background-color 0.2s ease-in-out; -} -.user_options-forms .forms_buttons-action:hover { - background-color: #e14641; -} -.user_options-forms .user_forms-signup, -.user_options-forms .user_forms-login { - position: absolute; - top: 70px; - left: 40px; - width: calc(100% - 80px); - opacity: 0; - visibility: hidden; - transition: opacity 0.4s ease-in-out, visibility 0.4s ease-in-out, transform 0.5s ease-in-out; +.description { + font-size: 14px; + letter-spacing: 0.25px; + text-align: center; + line-height: 1.6; } -.user_options-forms .user_forms-signup { - transform: translate3d(120px, 0, 0); + +.button { + width: 180px; + height: 50px; + border-radius: 25px; + margin-top: 50px; + font-weight: 700; + font-size: 14px; + letter-spacing: 1.15px; + background-color: #4B70E2; + color: #f9f9f9; + box-shadow: 8px 8px 16px #d1d9e6, -8px -8px 16px #f9f9f9; + border: none; + outline: none; } -.user_options-forms .user_forms-signup .forms_buttons { - justify-content: flex-end; + +.a-container { + z-index: 100; + left: calc(100% - 600px); } -.user_options-forms .user_forms-login { - transform: translate3d(0, 0, 0); - opacity: 1; - visibility: visible; + +.b-container { + left: calc(100% - 600px); + z-index: 0; } -/** - * * Triggers - * */ -.user_options-forms.bounceLeft { - -webkit-animation: bounceLeft 1s forwards; - animation: bounceLeft 1s forwards; +.switch { + display: flex; + justify-content: center; + align-items: center; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 400px; + padding: 50px; + z-index: 200; + transition: 1.25s; + background-color: #ecf0f3; + overflow: hidden; + box-shadow: 4px 4px 10px #d1d9e6, -4px -4px 10px #d1d9e6; } -.user_options-forms.bounceLeft .user_forms-signup { - -webkit-animation: showSignUp 1s forwards; - animation: showSignUp 1s forwards; + +.switch_circle { + position: absolute; + width: 500px; + height: 500px; + border-radius: 50%; + background-color: #ecf0f3; + box-shadow: inset 8px 8px 12px #b8bec7, inset -8px -8px 12px #fff; + bottom: -60%; + left: -60%; + transition: 1.25s; } -.user_options-forms.bounceLeft .user_forms-login { - opacity: 0; - visibility: hidden; - transform: translate3d(-120px, 0, 0); + +.switch_circle-t { + top: -30%; + left: 60%; + width: 300px; + height: 300px; } -.user_options-forms.bounceRight { - -webkit-animation: bounceRight 1s forwards; - animation: bounceRight 1s forwards; + +.switch_container { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + position: absolute; + width: 400px; + padding: 50px 55px; + transition: 1.25s; } -/** - * * Responsive 990px - * */ -@media screen and (max-width: 990px) { - .user_options-forms { - min-height: 350px; - } - .user_options-forms .forms_buttons { - flex-direction: column; - } - .user_options-forms .user_forms-login .forms_buttons-action { - margin-top: 30px; - } - .user_options-forms .user_forms-signup, - .user_options-forms .user_forms-login { - top: 40px; - } - - .user_options-registered, - .user_options-unregistered { - padding: 50px 45px; - } +.switch_button { + cursor: pointer; } +.switch_button:hover, +.submit:hover { + box-shadow: 6px 6px 10px #d1d9e6, -6px -6px 10px #f9f9f9; + transform: scale(0.985); + transition: 0.25s; +} +.switch_button:active, +.switch_button:focus { + box-shadow: 2px 2px 6px #d1d9e6, -2px -2px 6px #f9f9f9; + transform: scale(0.97); + transition: 0.25s; +} + +.is-txr { + left: calc(100% - 400px); + transition: 1.25s; + transform-origin: left; +} + +.is-txl { + left: 0; + transition: 1.25s; + transform-origin: right; +} + +.is-z { + z-index: 200; + transition: 1.25s; +} + +.is-hidden { + visibility: hidden; + opacity: 0; + position: absolute; + transition: 1.25s; +} + +.is-gx { + animation: is-gx 1.25s; +} + +@keyframes is-gx { + + 0%, + 10%, + 100% { + width: 400px; + } + + 30%, + 50% { + width: 500px; + } +} + +.iconfont { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + cursor: pointer; +} + +.logo { + position: absolute; + top: -10px; + left: -10px; + width: 150px; + height: 150px; +} + +#video-background { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; /* 纭繚瑙嗛濉厖鏁翠釜瑙嗗彛 */ + z-index: -1; /* 灏嗚棰戠疆浜庡唴瀹瑰悗闈� */ +} + +.toast-list { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 9999; + pointer-events: none; + display: flex; + flex-direction: column; + align-items: center; +} </style> \ No newline at end of file