feat(player): 新增长页面一键回到顶部按钮
- 在 MainLayout 主滚动区挂载浮动回顶按钮,投注列表与赛事详情等长页通用 - 滚动超过阈值后显示,支持平滑回顶与底部导航避让 - 补充中/英/马来语文案 common.back_to_top Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
79
apps/player/src/components/BackToTopButton.vue
Normal file
79
apps/player/src/components/BackToTopButton.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<script setup lang="ts">
|
||||
import { toRef } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useBackToTop } from '../composables/useBackToTop';
|
||||
|
||||
const props = defineProps<{
|
||||
scrollEl: HTMLElement | null;
|
||||
aboveNav?: boolean;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const { visible, scrollToTop } = useBackToTop(toRef(props, 'scrollEl'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="back-top-fade">
|
||||
<button
|
||||
v-if="visible"
|
||||
type="button"
|
||||
class="back-top-btn"
|
||||
:class="{ 'above-nav': aboveNav }"
|
||||
:aria-label="t('common.back_to_top')"
|
||||
@click="scrollToTop"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M12 5.5 6 11.5l1.4 1.4 3.6-3.6V18h2V9.3l3.6 3.6 1.4-1.4-6-6Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.back-top-btn {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: calc(12px + env(safe-area-inset-bottom, 0px));
|
||||
z-index: 120;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-gold-soft, rgba(200, 168, 78, 0.35));
|
||||
background: rgba(17, 17, 17, 0.92);
|
||||
color: var(--primary-light, #c8a84e);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.back-top-btn.above-nav {
|
||||
bottom: calc(68px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.back-top-btn:active {
|
||||
background: rgba(200, 168, 78, 0.16);
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.back-top-btn svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.back-top-fade-enter-active,
|
||||
.back-top-fade-leave-active {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.back-top-fade-enter-from,
|
||||
.back-top-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
</style>
|
||||
38
apps/player/src/composables/useBackToTop.ts
Normal file
38
apps/player/src/composables/useBackToTop.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { onMounted, onUnmounted, ref, watch, type Ref } from 'vue';
|
||||
|
||||
export function useBackToTop(
|
||||
scrollEl: Ref<HTMLElement | null>,
|
||||
options?: { threshold?: number },
|
||||
) {
|
||||
const visible = ref(false);
|
||||
const threshold = options?.threshold ?? 320;
|
||||
|
||||
function onScroll() {
|
||||
const el = scrollEl.value;
|
||||
visible.value = !!el && el.scrollTop > threshold;
|
||||
}
|
||||
|
||||
function scrollToTop() {
|
||||
scrollEl.value?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function bind(el: HTMLElement | null) {
|
||||
el?.addEventListener('scroll', onScroll, { passive: true });
|
||||
onScroll();
|
||||
}
|
||||
|
||||
function unbind(el: HTMLElement | null) {
|
||||
el?.removeEventListener('scroll', onScroll);
|
||||
}
|
||||
|
||||
onMounted(() => bind(scrollEl.value));
|
||||
|
||||
onUnmounted(() => unbind(scrollEl.value));
|
||||
|
||||
watch(scrollEl, (el, prev) => {
|
||||
unbind(prev);
|
||||
bind(el);
|
||||
});
|
||||
|
||||
return { visible, scrollToTop, onScroll };
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export default {
|
||||
no_more: 'No more',
|
||||
load_failed: 'Failed to load',
|
||||
retry: 'Retry',
|
||||
back_to_top: 'Back to top',
|
||||
},
|
||||
nav: { home: 'Home', bet: 'Bet', bet_history: 'History', wallet: 'Wallet', profile: 'Profile' },
|
||||
home: {
|
||||
|
||||
@@ -7,6 +7,7 @@ export default {
|
||||
no_more: 'Tiada lagi',
|
||||
load_failed: 'Gagal dimuat',
|
||||
retry: 'Cuba lagi',
|
||||
back_to_top: 'Kembali ke atas',
|
||||
},
|
||||
nav: {
|
||||
home: 'Laman Utama',
|
||||
|
||||
@@ -7,6 +7,7 @@ export default {
|
||||
no_more: '没有更多了',
|
||||
load_failed: '加载失败',
|
||||
retry: '重试',
|
||||
back_to_top: '回到顶部',
|
||||
},
|
||||
nav: { home: '主页', bet: '投注', bet_history: '历史投注', wallet: '账单', profile: '我的' },
|
||||
home: {
|
||||
|
||||
@@ -9,6 +9,7 @@ import LocaleSwitcher from '../components/LocaleSwitcher.vue';
|
||||
import { useAppLocale } from '../composables/useAppLocale';
|
||||
import AnnouncementMarquee from '../components/AnnouncementMarquee.vue';
|
||||
import BottomNavIcon from '../components/BottomNavIcon.vue';
|
||||
import BackToTopButton from '../components/BackToTopButton.vue';
|
||||
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
|
||||
|
||||
const BetSlipDrawer = defineAsyncComponent(() => import('../components/BetSlipDrawer.vue'));
|
||||
@@ -178,6 +179,8 @@ watch(
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
||||
<BackToTopButton :scroll-el="mainRef" :above-nav="showBottomNav" />
|
||||
|
||||
<BetSlipDrawer v-model="slip.drawerOpen" />
|
||||
<CustomerServiceModal v-model="customerServiceOpen" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user