From 06d85953bade6a7ad6b4f4406acc934c5a49491d Mon Sep 17 00:00:00 2001 From: Mars <3361409208a@gmail.com> Date: Tue, 16 Jun 2026 10:07:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(player):=20=E6=96=B0=E5=A2=9E=E9=95=BF?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E4=B8=80=E9=94=AE=E5=9B=9E=E5=88=B0=E9=A1=B6?= =?UTF-8?q?=E9=83=A8=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 MainLayout 主滚动区挂载浮动回顶按钮,投注列表与赛事详情等长页通用 - 滚动超过阈值后显示,支持平滑回顶与底部导航避让 - 补充中/英/马来语文案 common.back_to_top Co-authored-by: Cursor --- .../player/src/components/BackToTopButton.vue | 79 +++++++++++++++++++ apps/player/src/composables/useBackToTop.ts | 38 +++++++++ apps/player/src/i18n/en-US.ts | 1 + apps/player/src/i18n/ms-MY.ts | 1 + apps/player/src/i18n/zh-CN.ts | 1 + apps/player/src/layouts/MainLayout.vue | 3 + 6 files changed, 123 insertions(+) create mode 100644 apps/player/src/components/BackToTopButton.vue create mode 100644 apps/player/src/composables/useBackToTop.ts diff --git a/apps/player/src/components/BackToTopButton.vue b/apps/player/src/components/BackToTopButton.vue new file mode 100644 index 0000000..0088661 --- /dev/null +++ b/apps/player/src/components/BackToTopButton.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/apps/player/src/composables/useBackToTop.ts b/apps/player/src/composables/useBackToTop.ts new file mode 100644 index 0000000..3c8fc40 --- /dev/null +++ b/apps/player/src/composables/useBackToTop.ts @@ -0,0 +1,38 @@ +import { onMounted, onUnmounted, ref, watch, type Ref } from 'vue'; + +export function useBackToTop( + scrollEl: Ref, + 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 }; +} diff --git a/apps/player/src/i18n/en-US.ts b/apps/player/src/i18n/en-US.ts index 5a89357..580607e 100644 --- a/apps/player/src/i18n/en-US.ts +++ b/apps/player/src/i18n/en-US.ts @@ -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: { diff --git a/apps/player/src/i18n/ms-MY.ts b/apps/player/src/i18n/ms-MY.ts index b32de58..3d5869b 100644 --- a/apps/player/src/i18n/ms-MY.ts +++ b/apps/player/src/i18n/ms-MY.ts @@ -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', diff --git a/apps/player/src/i18n/zh-CN.ts b/apps/player/src/i18n/zh-CN.ts index 7d92d2e..f1646eb 100644 --- a/apps/player/src/i18n/zh-CN.ts +++ b/apps/player/src/i18n/zh-CN.ts @@ -7,6 +7,7 @@ export default { no_more: '没有更多了', load_failed: '加载失败', retry: '重试', + back_to_top: '回到顶部', }, nav: { home: '主页', bet: '投注', bet_history: '历史投注', wallet: '账单', profile: '我的' }, home: { diff --git a/apps/player/src/layouts/MainLayout.vue b/apps/player/src/layouts/MainLayout.vue index 339d694..ce93df2 100644 --- a/apps/player/src/layouts/MainLayout.vue +++ b/apps/player/src/layouts/MainLayout.vue @@ -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( + +