feat(layout): 重构页面布局与音乐播放组件

将 PlayMusic 组件迁移至 layout/RightButton 并重构成通用右下角按钮组件,
提取音乐播放逻辑到独立 hook `usePlayMusic`,优化模态框提示逻辑并统一滚动行为。
This commit is contained in:
log1997
2025-12-04 17:38:52 +08:00
parent 9009eede02
commit 0d97c592e1
7 changed files with 171 additions and 154 deletions

View File

@@ -3,7 +3,7 @@ import { storeToRefs } from 'pinia'
import { onMounted, provide, ref } from 'vue' import { onMounted, provide, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { loadingKey, loadingState } from '@/components/Loading' import { loadingKey, loadingState } from '@/components/Loading'
import PlayMusic from '@/components/PlayMusic/index.vue' // import PlayMusic from '@/components/PlayMusic/index.vue'
import useStore from '@/store' import useStore from '@/store'
import { themeChange } from '@/utils' import { themeChange } from '@/utils'
@@ -14,74 +14,10 @@ const prizeConfig = useStore().prizeConfig
const system = useStore().system const system = useStore().system
const { getTheme: localTheme } = storeToRefs(globalConfig) const { getTheme: localTheme } = storeToRefs(globalConfig)
const { getPrizeConfig: prizeList } = storeToRefs(prizeConfig) const { getPrizeConfig: prizeList } = storeToRefs(prizeConfig)
const tipDialog = ref()
// 设置当前奖列表
function setCurrentPrize() {
if (prizeList.value.length <= 0) {
return
}
for (let i = 0; i < prizeList.value.length; i++) {
if (!prizeList.value[i].isUsed) {
prizeConfig.setCurrentPrize(prizeList.value[i])
break
}
}
}
// 判断是否手机端访问
function judgeMobile() {
const ua = navigator.userAgent
const isAndroid = ua.includes('Android') || ua.includes('Adr')
const isIOS = !!ua.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)
system.setIsMobile(isAndroid || isIOS)
return isAndroid || isIOS
}
// 判断是否chrome或者edge访问
function judgeChromeOrEdge() {
const ua = navigator.userAgent
const isChrome = ua.includes('Chrome')
const isEdge = ua.includes('Edg')
system.setIsChrome(isChrome)
return isChrome || isEdge
}
onMounted(() => {
themeChange(localTheme.value.name)
setCurrentPrize()
if (judgeMobile() || !judgeChromeOrEdge()) {
tipDialog.value.showModal()
}
})
</script> </script>
<template> <template>
<dialog id="my_modal_1" ref="tipDialog" class="border-none modal">
<div class="modal-box">
<h3 class="text-lg font-bold">
{{ t('dialog.titleTip') }}
</h3>
<p v-if="judgeMobile()" class="py-4">
{{ t('dialog.dialogPCWeb') }}
</p>
<p v-if=" !judgeChromeOrEdge()" class="py-4">
{{ t('dialog.dialogLatestBrowser') }}
</p>
<div class="modal-action">
<form method="dialog" class="flex justify-start w-full gap-3">
<!-- if there is a button in form, it will close the modal -->
<button class="btn">
{{ t('button.confirm') }}
</button>
</form>
</div>
</div>
</dialog>
<router-view /> <router-view />
<PlayMusic class="absolute right-0 bottom-1/2" />
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

1
src/components.d.ts vendored
View File

@@ -16,7 +16,6 @@ declare module 'vue' {
ImageSync: typeof import('./components/ImageSync/index.vue')['default'] ImageSync: typeof import('./components/ImageSync/index.vue')['default']
Loading: typeof import('./components/Loading/index.vue')['default'] Loading: typeof import('./components/Loading/index.vue')['default']
PageHeader: typeof import('./components/PageHeader/index.vue')['default'] PageHeader: typeof import('./components/PageHeader/index.vue')['default']
PlayMusic: typeof import('./components/PlayMusic/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
SvgIcon: typeof import('./components/SvgIcon/index.vue')['default'] SvgIcon: typeof import('./components/SvgIcon/index.vue')['default']

View File

@@ -1,10 +1,10 @@
<script setup lang='ts'> <script setup lang='ts'>
import { ChevronUp } from 'lucide-vue-next'
</script> </script>
<template> <template>
<div class="fixed z-50 flex items-center justify-center w-10 h-10 rounded-full shadow-lg cursor-pointer right-12 bottom-12 bg-slate-700 hover:bg-slate-600"> <div class="fixed z-50 flex items-center justify-center w-10 h-10 rounded-full shadow-lg cursor-pointer right-12 bottom-12 bg-slate-700 hover:bg-slate-600">
<svg-icon name="toTop" /> <ChevronUp />
</div> </div>
</template> </template>

View File

@@ -1,97 +1,25 @@
<script setup lang='ts'> <script setup lang='ts'>
import useStore from '@/store' import { ref } from 'vue'
import localforage from 'localforage'
import { storeToRefs } from 'pinia'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { usePlayMusic } from './usePlayMusic'
const { playMusic, currentMusic, nextPlay } = usePlayMusic()
const { t } = useI18n() const { t } = useI18n()
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const audioDbStore = localforage.createInstance({
name: 'audioStore',
})
const audio = ref(new Audio())
const settingRef = ref() const settingRef = ref()
// const audio = ref(new Audio()) // const audio = ref(new Audio())
const globalConfig = useStore().globalConfig
const { getMusicList: localMusicList, getCurrentMusic: currentMusic } = storeToRefs(globalConfig)
// const localMusicListValue = ref(localMusicList) // const localMusicListValue = ref(localMusicList)
async function play(item: any) {
if (!item) {
return
}
// if (!audio.value.paused && !skip) {
// audio.value.pause()
// return
// }
let audioUrl = ''
if (!item.url) {
return
}
if (item.url === 'Storage') {
audioUrl = await audioDbStore.getItem(item.name) as string
}
else {
audioUrl = item.url
}
audio.value.pause()
audio.value.src = audioUrl
audio.value.play()
}
function playMusic(item: any, skip = false) {
if (!item) {
return
}
if (!currentMusic.value.paused && !skip) {
globalConfig.setCurrentMusic(item, true)
return
}
globalConfig.setCurrentMusic(item, false)
}
function nextPlay() {
//
if (localMusicList.value.length >= 1) {
let index = localMusicList.value.findIndex((item: any) => item.name === currentMusic.value.item.name)
index++
if (index >= localMusicList.value.length) {
index = 0
}
globalConfig.setCurrentMusic(localMusicList.value[index], false)
}
}
//
function onPlayEnd() {
audio.value.addEventListener('ended', nextPlay)
}
function enterConfig() { function enterConfig() {
router.push('/log-lottery/config') router.push('/log-lottery/config')
} }
function enterHome() { function enterHome() {
router.push('/log-lottery') router.push('/log-lottery')
} }
onMounted(() => {
globalConfig.setCurrentMusic(localMusicList.value[0], true)
onPlayEnd()
// 使audio
})
onUnmounted(() => {
audio.value.removeEventListener('ended', nextPlay)
})
watch(currentMusic, (val: any) => {
if (!val.paused && audio.value) {
play(val.item)
}
else {
audio.value.pause()
}
}, { deep: true })
</script> </script>
<template> <template>

View File

@@ -0,0 +1,85 @@
import localforage from 'localforage'
import { storeToRefs } from 'pinia'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import useStore from '@/store'
export function usePlayMusic() {
const audioDbStore = localforage.createInstance({
name: 'audioStore',
})
const globalConfig = useStore().globalConfig
const { getMusicList: localMusicList, getCurrentMusic: currentMusic } = storeToRefs(globalConfig)
const audio = ref(new Audio())
async function play(item: any) {
if (!item) {
return
}
// if (!audio.value.paused && !skip) {
// audio.value.pause()
// return
// }
let audioUrl = ''
if (!item.url) {
return
}
if (item.url === 'Storage') {
audioUrl = await audioDbStore.getItem(item.name) as string
}
else {
audioUrl = item.url
}
audio.value.pause()
audio.value.src = audioUrl
audio.value.play()
}
function playMusic(item: any, skip = false) {
if (!item) {
return
}
if (!currentMusic.value.paused && !skip) {
globalConfig.setCurrentMusic(item, true)
return
}
globalConfig.setCurrentMusic(item, false)
}
function nextPlay() {
// 播放下一首
if (localMusicList.value.length >= 1) {
let index = localMusicList.value.findIndex((item: any) => item.name === currentMusic.value.item.name)
index++
if (index >= localMusicList.value.length) {
index = 0
}
globalConfig.setCurrentMusic(localMusicList.value[index], false)
}
}
// 监听播放成后开始下一首
function onPlayEnd() {
audio.value.addEventListener('ended', nextPlay)
}
onMounted(() => {
globalConfig.setCurrentMusic(localMusicList.value[0], true)
onPlayEnd()
// 不使用空格控制audio
})
onUnmounted(() => {
audio.value.removeEventListener('ended', nextPlay)
})
watch(currentMusic, (val: any) => {
if (!val.paused && audio.value) {
play(val.item)
}
else {
audio.value.pause()
}
}, { deep: true })
return {
currentMusic,
playMusic,
nextPlay,
}
}

View File

@@ -1,32 +1,36 @@
<script setup lang="ts"> <script setup lang="ts">
import { useScroll } from '@vueuse/core' import { useScroll } from '@vueuse/core'
// import Header from './Header/index.vue';
// import Footer from './Footer/index.vue';
import { ref } from 'vue' import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import CustomModal from '@/components/Dialog/index.vue'
import { Loading } from '@/components/Loading' import { Loading } from '@/components/Loading'
import ToTop from '@/components/ToTop/index.vue' import ToTop from '@/components/ToTop/index.vue'
import RightButton from './RightButton/index.vue'
import { useMounted } from './useMounted'
const tipDialog = ref()
const { tipDesc } = useMounted(tipDialog)
const { t } = useI18n()
const mainContainer = ref<HTMLElement | null>(null) const mainContainer = ref<HTMLElement | null>(null)
const { y } = useScroll(mainContainer) const { y } = useScroll(mainContainer)
function scrollToTop() { function scrollToTop() {
y.value = 0 mainContainer.value?.scrollTo({
top: 0,
behavior: 'smooth',
})
} }
</script> </script>
<template> <template>
<div class="w-screen"> <div class="w-screen">
<!-- <header class="shadow-2xl head-container h-14">
<Header></Header>
</header> -->
<Loading /> <Loading />
<ToTop v-if="y > 400" @click="scrollToTop" /> <ToTop v-if="y > 400" @click="scrollToTop" />
<main ref="mainContainer" class="box-content w-screen h-screen overflow-x-hidden overflow-y-auto main-container"> <main ref="mainContainer" class="box-content w-screen h-screen overflow-x-hidden overflow-y-auto main-container">
<router-view class="h-full main-container-content" /> <router-view class="h-full main-container-content" />
</main> </main>
<!-- <footer class="w-screen footer-container"> <RightButton class="absolute right-0 bottom-1/2" />
<Footer></Footer> <CustomModal ref="tipDialog" :title="t('dialog.titleTip')" :desc="tipDesc" />
</footer> -->
</div> </div>
</template> </template>

65
src/layout/useMounted.ts Normal file
View File

@@ -0,0 +1,65 @@
import type { Ref } from 'vue'
import { storeToRefs } from 'pinia'
import { onMounted, provide, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { loadingKey, loadingState } from '@/components/Loading'
import useStore from '@/store'
import { themeChange } from '@/utils'
export function useMounted(tipDialog: Ref<any>) {
provide(loadingKey, loadingState)
const globalConfig = useStore().globalConfig
const prizeConfig = useStore().prizeConfig
const system = useStore().system
const { getTheme: localTheme } = storeToRefs(globalConfig)
const { getPrizeConfig: prizeList } = storeToRefs(prizeConfig)
const tipDesc = ref('')
const { t } = useI18n()
// 设置当前奖列表
function setCurrentPrize() {
if (prizeList.value.length <= 0) {
return
}
for (let i = 0; i < prizeList.value.length; i++) {
if (!prizeList.value[i].isUsed) {
prizeConfig.setCurrentPrize(prizeList.value[i])
break
}
}
}
// 判断是否手机端访问
function judgeMobile() {
const ua = navigator.userAgent
const isAndroid = ua.includes('Android') || ua.includes('Adr')
const isIOS = !!ua.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)
system.setIsMobile(isAndroid || isIOS)
return isAndroid || isIOS
}
// 判断是否chrome或者edge访问
function judgeChromeOrEdge() {
const ua = navigator.userAgent
const isChrome = ua.includes('Chrome')
const isEdge = ua.includes('Edg')
system.setIsChrome(isChrome)
return isChrome || isEdge
}
onMounted(() => {
themeChange(localTheme.value.name)
setCurrentPrize()
if (judgeMobile()) {
tipDialog.value.showDialog()
tipDesc.value = t('dialog.dialogPCWeb')
}
else if (!judgeChromeOrEdge()) {
tipDialog.value.showDialog()
tipDesc.value = t('dialog.dialogLatestBrowser')
}
})
return { tipDesc }
}