conflict-nuxt-4/app/components/auto-import/TabBar.vue
2026-02-12 11:24:27 +03:30

279 lines
8.3 KiB
Vue
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="relative flex items-center" :style="{ width: customWidth }">
<!-- نسخه دسکتاپ / تبلت: همان قبلی -->
<div v-if="!isMobile" class="w-full">
<UButton
v-if="hasOverflow"
icon="i-heroicons-chevron-left"
variant="ghost"
size="xs"
class="absolute -left-10 top-1/2 -translate-y-1/2 z-10 flex-shrink-0 bg-light-primary dark:bg-dark-primary-800 shadow-md border border-gray-200 dark:border-dark-primary-700"
@click="scrollLeft"
/>
<div
ref="tabsContainer"
class="flex overflow-x-auto scrollbar-hide w-full justify-start"
@wheel.prevent="handleWheelScroll"
>
<div class="flex items-center">
<button
v-for="tab in getListTabs()"
:key="tab.id"
@click="handleTabClick(tab)"
:class="getTabClass(activeTab === tab.id)"
:data-active="activeTab === tab.id"
>
<UIcon
v-if="tab.icon"
:name="tab.icon"
class="w-5 h-5 flex-shrink-0"
/>
<span class="text-sm font-medium">{{ tab.label }}</span>
</button>
</div>
</div>
<UButton
v-if="hasOverflow"
icon="i-heroicons-chevron-right"
variant="ghost"
size="xs"
class="absolute -right-10 top-1/2 -translate-y-1/2 z-10 flex-shrink-0 bg-light-primary dark:bg-dark-primary-800 shadow-md border border-gray-200 dark:border-dark-primary-700"
@click="scrollRight"
/>
</div>
<!-- نسخه موبایل -->
<div
v-else
class="fixed inset-x-0 bottom-0 z-50 bg-light-primary dark:bg-dark-primary-800 border-t border-gray-200 dark:border-dark-primary-700 shadow-lg pb-safe"
>
<!-- اگر ۵ یا کمتر → justify-around و بدون اسکرول -->
<!-- اگر بیشتر از ۵ → اسکرول افقی با flex-nowrap -->
<div
class="flex items-center"
:class="getListTabs().length <= 5 ? '' : 'flex-nowrap'"
>
<button
v-for="tab in getListTabs()"
:key="tab.id"
@click="handleTabClick(tab)"
class="flex flex-col items-center justify-center py-3 transition-colors relative mx-4 "
:class="[
getListTabs().length <= 5
? 'flex-1 min-w-0 min-h-[56px]'
: 'px-4 min-w-[80px] min-h-[56px] flex-shrink-0',
]"
>
<!-- آیکن -->
<UIcon
v-if="tab.icon"
:name="tab.icon"
class="w-6 h-6 mb-1"
:class="
activeTab === tab.id
? 'text-primary-600 dark:text-primary-400'
: 'text-gray-600 dark:text-gray-400'
"
/>
<!-- اگر آیکن نبود، فقط متن با فاصله مناسب -->
<div v-else class="h-7"></div>
<!-- متن -->
<span
class="text-xs font-medium whitespace-nowrap"
:class="
activeTab === tab.id
? 'text-primary-600 dark:text-primary-400'
: 'text-gray-600 dark:text-gray-400'
"
>
{{ tab.label }}
</span>
<!-- اندیکاتور فعال (خط زیر تب) -->
<div
v-if="activeTab === tab.id"
class="absolute bottom-0 w-12 h-1 bg-primary-600 dark:bg-primary-400 rounded-t-full"
/>
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick, watch } from "vue";
const props = defineProps({
tabs: { type: Array, default: () => [] },
activeTab: { type: String, default: "" },
width: { type: String, default: "40em" },
mode: {
type: String,
default: "soft",
validator: (v) => ["soft", "chrome"].includes(v),
},
});
const emit = defineEmits(["tab-change", "update:activeTab"]);
const tabsContainer = ref(null);
const hasOverflow = ref(false);
const isMobile = ref(false);
const customWidth =
props.width.includes("px") ||
props.width.includes("em") ||
props.width.includes("%")
? props.width
: `${props.width}px`;
// بروزرسانی وضعیت موبایل (امن برای SSR)
const updateMobileStatus = () => {
if (typeof window === "undefined") return;
isMobile.value = window.innerWidth < 768;
};
function getListTabs(){
const config = useRuntimeConfig();
const IS_DEVLOP_MODE = config.public.IS_DEVLOP_MODE || 1;
let result = {}
result = props.tabs.filter((el) => (!el.develop || el.develop == IS_DEVLOP_MODE) )
// console.log("TabBar IS_DEVLOP_MODE ", IS_DEVLOP_MODE, result);
return result
}
const getTabClass = (isActive) => {
const base =
"flex items-center justify-center gap-2 px-6 py-3 cursor-pointer whitespace-nowrap transition-all duration-200 flex-shrink-0 min-w-[140px] w-[140px]";
if (!isActive) {
return `${base} border-transparent text-dark-primary-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-light-primary hover:bg-gray-50 dark:hover:bg-dark-primary-800`;
}
if (props.mode === "chrome") {
return `${base} py-4 text-gray-900 dark:text-light-primary bg-light-primary dark:bg-dark-primary-800 rounded-t-xl`;
}
return `${base} border-primary-500 text-primary-600 dark:text-primary-400 bg-primary-50 dark:bg-primary-900/20 border-b-2`;
};
const handleTabClick = (tab) => {
emit("update:activeTab", tab.id);
emit("tab-change", tab);
if (!isMobile.value) {
nextTick(scrollToActiveTab);
}
};
// توابع دسکتاپ (همان قبلی)
const checkOverflow = () => {
if (!tabsContainer.value || isMobile.value) {
hasOverflow.value = false;
return;
}
const container = tabsContainer.value;
hasOverflow.value = container.scrollWidth > container.clientWidth + 1;
};
const scrollLeft = () => {
if (!tabsContainer.value) return;
tabsContainer.value.scrollLeft -= 200;
requestAnimationFrame(checkOverflow);
};
const scrollRight = () => {
if (!tabsContainer.value) return;
const container = tabsContainer.value;
const maxScroll = container.scrollWidth - container.clientWidth;
container.scrollLeft = Math.min(maxScroll, container.scrollLeft + 200);
requestAnimationFrame(checkOverflow);
};
const scrollToActiveTab = () => {
if (!tabsContainer.value || isMobile.value) return;
const container = tabsContainer.value;
const activeEl = container.querySelector('[data-active="true"]');
if (!activeEl) return;
const containerRect = container.getBoundingClientRect();
const elRect = activeEl.getBoundingClientRect();
if (elRect.left < containerRect.left) {
container.scrollLeft += elRect.left - containerRect.left - 16;
} else if (elRect.right > containerRect.right) {
container.scrollLeft += elRect.right - containerRect.right + 16;
}
};
const handleWheelScroll = (event) => {
if (!tabsContainer.value || isMobile.value) return;
tabsContainer.value.scrollLeft += event.deltaY;
requestAnimationFrame(checkOverflow);
};
const handleResize = () => {
updateMobileStatus();
nextTick(() => {
checkOverflow();
if (!isMobile.value) scrollToActiveTab();
});
};
watch(
() => props.activeTab,
() => {
if (!isMobile.value) nextTick(scrollToActiveTab);
}
);
watch(
() => props.tabs,
() => {
nextTick(checkOverflow);
}
);
onMounted(() => {
updateMobileStatus();
window.addEventListener("resize", handleResize);
if (tabsContainer.value) {
tabsContainer.value.addEventListener("scroll", checkOverflow);
}
requestAnimationFrame(() => {
checkOverflow();
if (!isMobile.value) scrollToActiveTab();
});
});
onUnmounted(() => {
window.removeEventListener("resize", handleResize);
if (tabsContainer.value) {
tabsContainer.value.removeEventListener("scroll", checkOverflow);
}
});
</script>
<style scoped>
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
scroll-behavior: smooth;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.pb-safe {
padding-bottom: env(safe-area-inset-bottom);
}
</style>