Skip to content

Commit

Permalink
feat(client): add mobile-friendly responsive design for index page #22
Browse files Browse the repository at this point in the history
- Implement mobile-specific tab navigation for categories- Update scroll watcher to support mobile
interaction- Adjust layout and styling for mobile view- Add dynamic tab switching based on scroll
position- Enhance mobile user experience with responsive components
  • Loading branch information
myltx committed Feb 28, 2025
1 parent 518935b commit 9fa2a3a
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 53 deletions.
5 changes: 4 additions & 1 deletion apps/client/src/components/Footer/index.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<template>
<div class="text-center text-gray-500 text-sm py-2">
<div
class="text-center text-gray-500 text-sm"
:class="isMobile() ? 'pt-1' : 'py-2'"
>
<!-- <UDivider :label="$config.public.projectName" :ui="{
}" /> -->
Expand Down
81 changes: 60 additions & 21 deletions apps/client/src/composables/useScrollWatcher.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// composables/useScrollWatcher.ts
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';

export function useScrollWatcher() {
const selectedAnchor = ref(''); // 当前选中的锚点
const thresholdDistance = 100; // 设置标题距离顶部的最小距离(单位:px),可以根据需求调整
const thresholdDistance = 100; // 设置标题距离顶部的最小距离(单位:px)
const headerHeight = ref(0); // 页面上方固定元素的高度
const debounceTimeout = 200; // 防抖延迟时间(单位:毫秒)

let scrollTimeout: number | null = null; // 防抖定时器

// 获取页面上方固定元素的高度
const setHeaderHeight = () => {
Expand All @@ -14,43 +16,80 @@ export function useScrollWatcher() {
}
};

// 获取距离顶部最近的锚点标题
const getNearestAnchor = () => {
const titles = document.querySelectorAll('.anchor-title');
let nearest = null;
let minDistance = Infinity;

titles.forEach((title) => {
const rect = title.getBoundingClientRect();
const distance = rect.top - headerHeight.value; // 计算与顶部的距离
if (distance >= 0 && distance < minDistance) {
nearest = title;
minDistance = distance;
}
});

return nearest;
};

// 更新选中的锚点
const updateSelectedAnchor = () => {
const nearest = getNearestAnchor();
if (nearest) {
selectedAnchor.value = nearest.id;
}
};

// 延迟触发选中更新
const handleScroll = () => {
// 如果有已经存在的定时器,清除
if (scrollTimeout) {
clearTimeout(scrollTimeout);
}

// 设置一个新的定时器,在滚动停止一定时间后更新选中的锚点
scrollTimeout = setTimeout(() => {
updateSelectedAnchor(); // 滚动结束后更新选中的锚点
}, debounceTimeout);
};

// 观察新的标题元素
const observeTitles = () => {
nextTick(() => {
const newTitles = document.querySelectorAll('.anchor-title');
newTitles.forEach((title) => observer.observe(title));
});
};

// 使用 IntersectionObserver 来监听元素是否进入视口
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
const rect = entry.target.getBoundingClientRect();

// 判断目标 title 元素与视口顶部的距离
if (rect.top <= headerHeight.value + thresholdDistance) {
// 如果标题元素距离顶部小于等于阈值并考虑了上方元素的高度
selectedAnchor.value = entry.target.id;
}
});
},
{
threshold: 0, // 设置最低阈值,当元素至少显示 10% 时才会触发
threshold: 0,
}
);

// 延迟观察新渲染的元素
// 定义一个函数,用于观察锚点标题
const observeTitles = () => {
// 使用nextTick函数,在下一个事件循环中执行
nextTick(() => {
// 获取所有的锚点标题
const newTitles = document.querySelectorAll('.anchor-title');
// 遍历所有的锚点标题
newTitles.forEach((title) => observer.observe(title));
});
};

onMounted(() => {
setHeaderHeight(); // 在组件挂载时获取上方元素的高度
observeTitles();
// observeTitles(); // 观察页面中的标题
window.addEventListener('scroll', handleScroll); // 添加滚动事件监听
});

onBeforeUnmount(() => {
observer.disconnect(); // 销毁时断开监听
if (scrollTimeout) {
clearTimeout(scrollTimeout); // 清除定时器
}
window.removeEventListener('scroll', handleScroll); // 移除滚动事件监听
observer.disconnect(); // 销毁时断开 IntersectionObserver 监听
});

// 手动滚动到指定锚点
Expand Down
82 changes: 51 additions & 31 deletions apps/client/src/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,23 @@ const categoriesKey = 'categories-';
const { openDialog } = useDialog();
const onChangeTab = (id: number) => {
loading.value = true;
const onChangeTab = (id: number, type: string = 'manual') => {
// loading.value = true;
activeTab.value = id;
// 滚动到当前 Tab 的可见区域
const tabElement = document.querySelector(`.tab[data-id="${id}"]`);
if (tabElement) {
tabElement.scrollIntoView({ behavior: 'smooth', inline: 'center' });
}
type === 'manual' && scrollToSection(`${categoriesKey}${id}`);
// getWebSites();
};
watchEffect(() => {
if (isMobile()) {
const id = Number(selectedAnchor.value.replace(categoriesKey, ''));
onChangeTab(id, 'auto');
}
});
const getSelectData = async () => {
const { data: categorysData } = await getCategoryList();
Expand Down Expand Up @@ -67,10 +74,8 @@ const goLink = (data: any) => {
};
const getWebSites = () => {
getWebsiteQueryAllGroup().then((res) => {
console.log(res, 'r');
websites.value = res.data.groupedData;
categorys.value = res.data.groupedData.map((item: any) => item.categories);
console.log(categorys.value, 'categorys.value');
loading.value = false;
observeTitles();
});
Expand Down Expand Up @@ -125,7 +130,7 @@ onMounted(async () => {

<template>
<div class="h-100% flex justify-between w-full">
<div class="w-80 bg-bgColor px-2">
<div class="bg-bgColor px-4 w-60" v-if="!isMobile()">
<div
class="cursor-pointer py-2 flex items-center gap-2 hover:text-#0066FF"
:class="{
Expand All @@ -141,8 +146,10 @@ onMounted(async () => {
</div>
<div class="flex-grow-1 h-100%">
<!-- 内容区顶部 -->
<!-- web端选择器样式 -->
<div
class="h-48 bg-bgColor py-4 b-l-1 b-solid b-bColor page-header shadow dark:shadow-otherBgColor 100 backdrop-blur shadow-"
v-if="!isMobile()"
>
<div class="px-30 flex gap-10">
<div
Expand Down Expand Up @@ -183,7 +190,34 @@ onMounted(async () => {
</div>
</div>
</div>
<div class="h-77.7% overflow-y-auto bg-otherBgColor px-30 pb-5">
<!-- 移动端选择器样式 -->
<div
class="px-2 mt-2 h-auto shadow-md rounded-lg w-100vw overflow-x-hidden"
v-else
>
<div
class="w-99% flex items-center whitespace-nowrap overflow-x-auto h-100% py-2"
>
<div
v-for="tab in categorys"
:key="tab.id"
:data-id="tab.id"
class="tab p-5 cursor-pointer text-4 h-10 rounded-5 flex items-center justify-center mr-3 bg-bgColor shadow hover:text-blue hover:font-500"
:class="[
activeTab === tab.id
? 'text-blue-500 border-blue-500 font-500'
: 'text-gray-500 border-gray-500',
]"
@click="onChangeTab(tab.id)"
>
{{ tab.name }}
</div>
</div>
</div>
<div
class="overflow-y-auto bg-otherBgColor pb-5"
:class="isMobile() ? 'h-92% px-8' : 'h-77.7% px-30'"
>
<div
class=""
v-for="categories in websites"
Expand Down Expand Up @@ -240,7 +274,8 @@ onMounted(async () => {
class="mt-2 text-2 text-gray-500 flex items-center justify-between"
>
<span class="mr-2 text-sm flex items-center">
👀
<!-- 👀 -->
<Icon name="line-md:watch" class="text-xl mr-1" />
{{ website.visitCount }}
</span>
<div class="flex items-center">
Expand All @@ -261,29 +296,7 @@ onMounted(async () => {
</div>

<!-- <div class="h-100%">
<div
class="px-2 mt-2 h-auto mx-2 shadow-md rounded-lg w-100% overflow-x-hidden"
>
<div
class="w-99% flex items-center whitespace-nowrap overflow-x-auto h-100% py-2"
>
<div
v-for="tab in categorys"
:key="tab.id"
:data-id="tab.id"
class="tab p-5 cursor-pointer text-4 h-10 rounded-5 flex items-center justify-center mr-3 shadow hover:text-blue hover:font-500"
:class="[
tab.id == activeTab
? 'text-blue-500 border-blue-500 font-500'
: 'text-gray-500 border-gray-500',
$colorMode.value === 'dark' ? 'bg-gray-800' : '',
]"
@click="onChangeTab(tab.id)"
>
{{ tab.name }}
</div>
</div>
</div>
<div class="mt-5 overflow-y-auto h-89%">
<div
class="grid gap-5 w-full justify-center"
Expand Down Expand Up @@ -373,8 +386,15 @@ body {
color: #433422;
}
.item {
transition:
transform 0.2s ease-in-out,
opacity 0.2s ease-in-out;
}
.item:hover {
transform: scale(1.05);
transition: transform 0.2s ease-in-out;
opacity: 0.95;
/* 添加透明度变化,使放大的效果更自然 */
}
</style>

0 comments on commit 9fa2a3a

Please sign in to comment.