Files
imen_pishghaman/apps/admin/src/layouts/AdminLayout.vue
T

1049 lines
33 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { ROLE_LABELS } from "@imen/shared";
import { computed, h, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { api } from "@/api/http";
import VuesaxIcon from "@/components/VuesaxIcon.vue";
import { useAuthStore } from "@/stores/auth";
import { SITE_URL } from "@/utils/format";
const auth = useAuthStore();
const route = useRoute();
const router = useRouter();
const drawer = ref(false);
const unreadCount = ref(0);
async function fetchUnread() {
try {
const res = await api<{ total: number }>("/admin/messages", {
query: { status: "NEW", pageSize: 1 },
});
unreadCount.value = res.total;
} catch {
// ignore
}
}
onMounted(fetchUnread);
watch(() => route.fullPath, fetchUnread);
/** Icon badge box + Vuesax glyph (keeps pre-swap menu structure) */
const menuIcon = (name: string) => () =>
h("span", { class: "admin-menu__icon" }, [h(VuesaxIcon, { name, size: 20 })]);
const items = computed(() => [
{ key: "dashboard", icon: menuIcon("chart-square"), label: "داشبورد" },
{ key: "products", icon: menuIcon("bag-2"), label: "محصولات" },
{ key: "categories", icon: menuIcon("category"), label: "دسته‌بندی‌ها" },
{ key: "sections", icon: menuIcon("row-vertical"), label: "بخش‌های سایت" },
{ key: "messages", icon: menuIcon("sms"), label: "پیام‌های تماس" },
{ key: "media", icon: menuIcon("gallery"), label: "کتابخانه رسانه" },
...(auth.isAdmin ? [{ key: "users", icon: menuIcon("profile-2user"), label: "کاربران" }] : []),
]);
/** menu entry to highlight for nested pages (products/12 → products) */
const MENU_OF: Record<string, string> = {
"product-new": "products",
"product-edit": "products",
"category-new": "categories",
"category-edit": "categories",
"section-edit": "sections",
};
const PAGE_TITLES: Record<string, string> = {
dashboard: "داشبورد",
products: "محصولات",
"product-new": "محصول جدید",
"product-edit": "ویرایش محصول",
categories: "دسته‌بندی‌ها",
"category-new": "دسته‌بندی جدید",
"category-edit": "ویرایش دسته‌بندی",
sections: "بخش‌های سایت",
"section-edit": "ویرایش بخش",
messages: "پیام‌های تماس",
media: "کتابخانه رسانه",
users: "کاربران",
profile: "حساب کاربری",
};
const selected = computed(() => {
const name = String(route.name ?? "");
return [MENU_OF[name] ?? name];
});
const pageTitle = computed(() => PAGE_TITLES[String(route.name ?? "")] ?? "پنل مدیریت");
const userInitial = computed(() => (auth.user?.name?.trim().charAt(0) || "ک").slice(0, 1));
const isChildRoute = computed(() => {
const name = String(route.name ?? "");
return ["product-new", "product-edit", "category-new", "category-edit", "section-edit"].includes(name);
});
const parentBack = computed(() => {
const name = String(route.name ?? "");
if (name.startsWith("product")) return { name: "products", label: "محصولات" };
if (name.startsWith("category")) return { name: "categories", label: "دسته‌بندی‌ها" };
if (name.startsWith("section")) return { name: "sections", label: "بخش‌های سایت" };
return { name: "dashboard", label: "داشبورد" };
});
const go = ({ key }: { key: string | number }) => {
drawer.value = false;
void router.push({ name: String(key) });
};
const navigateTo = (name: string) => {
drawer.value = false;
void router.push({ name });
};
watch(() => route.fullPath, () => (drawer.value = false));
</script>
<template>
<a-layout class="admin-shell">
<!-- Desktop Sider (lg and up) -->
<a-layout-sider
:width="248"
breakpoint="lg"
collapsed-width="0"
:trigger="null"
class="admin-sider !hidden lg:!block"
>
<div class="admin-sider__brand">
<span class="admin-sider__logo">
<img src="/logo.png" alt="" class="size-7 object-contain" />
</span>
<div class="leading-tight">
<div class="text-[14px] font-bold text-[#0f172a]">ایمن پیشگامان</div>
<div class="text-[10px] font-medium tracking-wide text-[#94a3b8]">پنل مدیریت</div>
</div>
</div>
<div class="admin-sider__nav">
<div class="admin-sider__label">منوی اصلی</div>
<a-menu
class="admin-menu"
mode="inline"
:selected-keys="selected"
:items="items"
@click="go"
/>
</div>
</a-layout-sider>
<!-- Mobile App Hub Drawer (Rich App Grid) -->
<a-drawer
v-model:open="drawer"
placement="right"
:width="320"
:closable="false"
class="mobile-hub-drawer"
:body-style="{ padding: 0, background: '#f8fafc' }"
>
<div class="flex flex-col h-full">
<!-- Hub User Profile Header -->
<div class="p-4 bg-white border-b border-slate-200/80">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2.5">
<span class="size-8 rounded-lg bg-orange-50 border border-orange-100 flex items-center justify-center">
<img src="/logo.png" alt="" class="size-5 object-contain" />
</span>
<div>
<div class="text-sm font-bold text-slate-800">ایمن پیشگامان</div>
<div class="text-[10px] text-slate-400">اپلیکیشن مدیریت</div>
</div>
</div>
<button
type="button"
class="size-8 rounded-full flex items-center justify-center text-slate-400 hover:text-slate-600 hover:bg-slate-100 transition"
@click="drawer = false"
>
<VuesaxIcon name="close-circle" :size="20" />
</button>
</div>
<div
class="flex items-center gap-3 p-2.5 rounded-xl bg-slate-50 border border-slate-100 cursor-pointer active:scale-[0.99] transition"
@click="navigateTo('profile')"
>
<span class="size-10 rounded-full bg-linear-to-br from-[#ff5a00] to-[#ff8a3d] text-white flex items-center justify-center font-bold text-sm shadow-xs">
{{ userInitial }}
</span>
<div class="min-w-0 flex-1">
<div class="text-xs font-bold text-slate-800 truncate">{{ auth.user?.name }}</div>
<div class="flex items-center gap-2 mt-0.5">
<span class="text-[10px] text-orange-600 font-medium">
{{ auth.user && ROLE_LABELS[auth.user.role] }}
</span>
<span class="text-[10px] text-slate-400 font-mono" dir="ltr">{{ auth.user?.phone }}</span>
</div>
</div>
<VuesaxIcon name="arrow-left" :size="14" class="text-slate-400" />
</div>
</div>
<!-- Hub Apps Grid Content -->
<div class="flex-1 overflow-y-auto p-4 space-y-4">
<div>
<div class="text-[11px] font-bold text-slate-400 mb-2.5 px-1">بخش‌ها و کاتالوگ</div>
<div class="grid grid-cols-2 gap-2">
<button
type="button"
class="flex flex-col items-center justify-center p-3 rounded-xl bg-white border border-slate-200/70 shadow-2xs hover:border-orange-200 active:bg-orange-50/40 transition"
:class="{ '!border-orange-500 !bg-orange-50/30 font-bold text-orange-600': route.name === 'dashboard' }"
@click="navigateTo('dashboard')"
>
<div class="size-9 rounded-lg bg-orange-50 text-orange-600 flex items-center justify-center mb-1.5">
<VuesaxIcon name="chart-square" :size="18" />
</div>
<span class="text-xs text-slate-700">داشبورد</span>
<span class="text-[9px] text-slate-400 mt-0.5">نمای کلی آمار</span>
</button>
<button
type="button"
class="flex flex-col items-center justify-center p-3 rounded-xl bg-white border border-slate-200/70 shadow-2xs hover:border-orange-200 active:bg-orange-50/40 transition"
:class="{ '!border-orange-500 !bg-orange-50/30 font-bold text-orange-600': selected.includes('products') }"
@click="navigateTo('products')"
>
<div class="size-9 rounded-lg bg-orange-50 text-orange-600 flex items-center justify-center mb-1.5">
<VuesaxIcon name="bag-2" :size="18" />
</div>
<span class="text-xs text-slate-700">محصولات</span>
<span class="text-[9px] text-slate-400 mt-0.5">کاتالوگ و موجودی</span>
</button>
<button
type="button"
class="flex flex-col items-center justify-center p-3 rounded-xl bg-white border border-slate-200/70 shadow-2xs hover:border-blue-200 active:bg-blue-50/40 transition"
:class="{ '!border-blue-500 !bg-blue-50/30 font-bold text-blue-600': selected.includes('categories') }"
@click="navigateTo('categories')"
>
<div class="size-9 rounded-lg bg-blue-50 text-blue-600 flex items-center justify-center mb-1.5">
<VuesaxIcon name="category" :size="18" />
</div>
<span class="text-xs text-slate-700">دسته‌بندی‌ها</span>
<span class="text-[9px] text-slate-400 mt-0.5">گروه‌های کالا</span>
</button>
<button
type="button"
class="flex flex-col items-center justify-center p-3 rounded-xl bg-white border border-slate-200/70 shadow-2xs hover:border-amber-200 active:bg-amber-50/40 transition relative"
:class="{ '!border-amber-500 !bg-amber-50/30 font-bold text-amber-600': route.name === 'messages' }"
@click="navigateTo('messages')"
>
<span v-if="unreadCount > 0" class="absolute top-2 end-2 size-2.5 rounded-full bg-orange-600 ring-2 ring-white animate-pulse" />
<div class="size-9 rounded-lg bg-amber-50 text-amber-600 flex items-center justify-center mb-1.5">
<VuesaxIcon name="sms" :size="18" />
</div>
<span class="text-xs text-slate-700">پیام‌های تماس</span>
<span class="text-[9px] text-slate-400 mt-0.5">درخواست‌های مشاوره</span>
</button>
</div>
</div>
<div>
<div class="text-[11px] font-bold text-slate-400 mb-2.5 px-1">محتوا و وب‌سایت</div>
<div class="grid grid-cols-2 gap-2">
<button
type="button"
class="flex flex-col items-center justify-center p-3 rounded-xl bg-white border border-slate-200/70 shadow-2xs hover:border-purple-200 active:bg-purple-50/40 transition"
:class="{ '!border-purple-500 !bg-purple-50/30 font-bold text-purple-600': route.name === 'media' }"
@click="navigateTo('media')"
>
<div class="size-9 rounded-lg bg-purple-50 text-purple-600 flex items-center justify-center mb-1.5">
<VuesaxIcon name="gallery" :size="18" />
</div>
<span class="text-xs text-slate-700">کتابخانه رسانه</span>
<span class="text-[9px] text-slate-400 mt-0.5">تصاویر WebP</span>
</button>
<button
type="button"
class="flex flex-col items-center justify-center p-3 rounded-xl bg-white border border-slate-200/70 shadow-2xs hover:border-emerald-200 active:bg-emerald-50/40 transition"
:class="{ '!border-emerald-500 !bg-emerald-50/30 font-bold text-emerald-600': selected.includes('sections') }"
@click="navigateTo('sections')"
>
<div class="size-9 rounded-lg bg-emerald-50 text-emerald-600 flex items-center justify-center mb-1.5">
<VuesaxIcon name="row-vertical" :size="18" />
</div>
<span class="text-xs text-slate-700">بخش‌های سایت</span>
<span class="text-[9px] text-slate-400 mt-0.5">تنظیمات صفحه اصلی</span>
</button>
</div>
</div>
<div v-if="auth.isAdmin">
<div class="text-[11px] font-bold text-slate-400 mb-2.5 px-1">مدیریت سیستم</div>
<div class="grid grid-cols-1 gap-2">
<button
type="button"
class="flex items-center gap-3 p-3 rounded-xl bg-white border border-slate-200/70 shadow-2xs hover:border-slate-300 active:bg-slate-50 transition text-right"
:class="{ '!border-orange-500 font-bold': route.name === 'users' }"
@click="navigateTo('users')"
>
<div class="size-9 rounded-lg bg-slate-100 text-slate-600 flex items-center justify-center shrink-0">
<VuesaxIcon name="profile-2user" :size="18" />
</div>
<div class="flex-1 min-w-0">
<div class="text-xs font-bold text-slate-800">مدیریت کاربران پنل</div>
<div class="text-[10px] text-slate-400">حساب‌ها و سطح دسترسی‌ها</div>
</div>
<VuesaxIcon name="arrow-left" :size="14" class="text-slate-400" />
</button>
</div>
</div>
</div>
<!-- Hub Footer System Actions -->
<div class="p-3 bg-white border-t border-slate-200/80 space-y-2">
<a
:href="SITE_URL"
target="_blank"
rel="noopener"
class="flex items-center justify-between p-2.5 rounded-lg text-slate-600 hover:bg-slate-50 text-xs font-semibold transition"
>
<span class="flex items-center gap-2">
<VuesaxIcon name="export" :size="16" class="text-slate-400" />
مشاهده وب‌سایت اصلی
</span>
<VuesaxIcon name="arrow-left" :size="12" class="text-slate-300" />
</a>
<button
type="button"
class="w-full flex items-center justify-center gap-2 p-2.5 rounded-lg text-rose-600 hover:bg-rose-50 text-xs font-semibold transition"
@click="auth.logout()"
>
<VuesaxIcon name="logout" :size="16" />
خروج از حساب کاربری
</button>
</div>
</div>
</a-drawer>
<a-layout>
<!-- Dedicated Mobile App Bar (Mobile Only) -->
<header class="mobile-app-bar lg:!hidden">
<div v-if="isChildRoute" class="flex items-center justify-between w-full">
<button
type="button"
class="mobile-app-bar__back-btn"
@click="router.push(parentBack)"
>
<VuesaxIcon name="arrow-right" :size="20" />
<span class="text-xs font-bold text-slate-700">بازگشت به {{ parentBack.label }}</span>
</button>
<div class="text-xs font-bold text-slate-800 truncate max-w-[140px]">
{{ pageTitle }}
</div>
</div>
<div v-else class="flex items-center justify-between w-full">
<div class="flex items-center gap-2.5">
<span class="size-8 rounded-lg bg-orange-50 border border-orange-100 flex items-center justify-center">
<img src="/logo.png" alt="" class="size-5 object-contain" />
</span>
<div class="leading-none">
<div class="text-xs font-extrabold text-slate-800">ایمن پیشگامان</div>
<div class="text-[10px] font-medium text-orange-600 mt-0.5">{{ pageTitle }}</div>
</div>
</div>
<div class="flex items-center gap-1.5">
<a
:href="SITE_URL"
target="_blank"
rel="noopener"
class="size-8 rounded-lg flex items-center justify-center text-slate-500 hover:text-orange-600 hover:bg-orange-50 transition"
title="مشاهده سایت"
>
<VuesaxIcon name="export" :size="18" />
</a>
<button
type="button"
class="size-8 rounded-lg bg-linear-to-br from-[#ff5a00] to-[#ff8a3d] text-white flex items-center justify-center font-bold text-xs shadow-xs"
@click="router.push({ name: 'profile' })"
>
{{ userInitial }}
</button>
</div>
</div>
</header>
<!-- Desktop Header (Desktop Only) -->
<a-layout-header class="admin-header !hidden lg:!flex">
<div class="admin-header__start">
<div class="admin-header__crumb">
<span class="admin-header__crumb-kicker">پنل مدیریت</span>
<span class="admin-header__crumb-sep" aria-hidden="true">/</span>
<span class="admin-header__crumb-page">{{ pageTitle }}</span>
</div>
</div>
<div class="admin-header__end">
<a
:href="SITE_URL"
target="_blank"
rel="noopener"
class="admin-header__site"
>
<VuesaxIcon name="export" :size="16" />
<span>مشاهده سایت</span>
</a>
<div class="admin-header__divider" aria-hidden="true" />
<a-dropdown placement="bottomLeft" :trigger="['click']">
<button type="button" class="admin-header__user">
<span class="admin-header__avatar">{{ userInitial }}</span>
<span class="admin-header__user-meta">
<span class="admin-header__user-name">{{ auth.user?.name }}</span>
<span v-if="auth.user" class="admin-header__user-role">
{{ ROLE_LABELS[auth.user.role] }}
</span>
</span>
<VuesaxIcon name="arrow-down" :size="12" class="admin-header__chevron" />
</button>
<template #overlay>
<a-menu class="admin-header__dropdown">
<a-menu-item key="profile" @click="router.push({ name: 'profile' })">
<VuesaxIcon name="user" :size="16" /> حساب کاربری
</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout" danger @click="auth.logout()">
<VuesaxIcon name="logout" :size="16" /> خروج
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</a-layout-header>
<!-- Content Viewport -->
<a-layout-content class="admin-content" :class="{ 'admin-content--has-bottom-nav': !isChildRoute }">
<div class="admin-page">
<RouterView v-slot="{ Component, route: r }">
<component :is="Component" :key="r.path" />
</RouterView>
</div>
</a-layout-content>
<!-- Purpose-Built Mobile Bottom Navigation Bar -->
<nav v-if="!isChildRoute" class="mobile-nav-bar lg:!hidden">
<button
type="button"
class="mobile-nav-item"
:class="{ 'mobile-nav-item--active': route.name === 'dashboard' }"
@click="navigateTo('dashboard')"
>
<div class="mobile-nav-item__icon">
<VuesaxIcon name="chart-square" :size="20" />
</div>
<span class="mobile-nav-item__label">داشبورد</span>
</button>
<button
type="button"
class="mobile-nav-item"
:class="{ 'mobile-nav-item--active': selected.includes('products') }"
@click="navigateTo('products')"
>
<div class="mobile-nav-item__icon">
<VuesaxIcon name="bag-2" :size="20" />
</div>
<span class="mobile-nav-item__label">محصولات</span>
</button>
<button
type="button"
class="mobile-nav-item"
:class="{ 'mobile-nav-item--active': route.name === 'messages' }"
@click="navigateTo('messages')"
>
<div class="mobile-nav-item__icon relative">
<VuesaxIcon name="sms" :size="20" />
<span
v-if="unreadCount > 0"
class="absolute -top-1 -end-1 size-2 rounded-full bg-orange-600 ring-2 ring-white"
/>
</div>
<span class="mobile-nav-item__label">پیام‌ها</span>
</button>
<button
type="button"
class="mobile-nav-item"
:class="{ 'mobile-nav-item--active': drawer || ['categories', 'sections', 'media', 'users', 'profile'].includes(String(route.name ?? '')) }"
@click="drawer = true"
>
<div class="mobile-nav-item__icon">
<VuesaxIcon name="category" :size="20" />
</div>
<span class="mobile-nav-item__label">ابزارها</span>
</button>
</nav>
</a-layout>
</a-layout>
</template>
<style scoped>
.admin-shell {
height: 100vh;
height: 100dvh;
overflow: hidden;
}
.admin-shell > :deep(.ant-layout) {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
overflow: hidden;
}
.admin-header {
position: sticky;
top: 0;
z-index: 20;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
height: 64px !important;
line-height: 1.3 !important;
padding-inline: 16px 20px !important;
background: rgba(255, 255, 255, 0.96) !important;
border-bottom: 1px solid #e8ecf2;
backdrop-filter: blur(10px);
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.02);
}
.admin-content {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
padding: 16px;
background: #f5f6fa;
box-sizing: border-box;
}
/* White panel locked to remaining viewport; pages manage their own inner scroll */
.admin-page {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
background: #ffffff;
border: 1px solid #e8ecf2;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
}
.admin-page > :deep(.page-header) {
flex: 0 0 auto !important;
overflow: visible !important;
padding: 16px 20px 14px !important;
border-bottom: 1px solid #eef1f6;
background: #ffffff;
}
.admin-page > :deep(*:not(.page-header)) {
flex: 1;
min-width: 0;
min-height: 0;
}
/* Pages that don't use AdminPage still scroll inside the panel */
.admin-page > :deep(*:not(.ap):not(.page-header)) {
overflow: auto;
padding: 16px;
box-sizing: border-box;
}
@media (min-width: 768px) {
.admin-header {
padding-inline: 24px !important;
}
}
.admin-header__start,
.admin-header__end {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.admin-header__menu-btn {
border-radius: 10px !important;
border: none !important;
color: #475569 !important;
background: transparent !important;
}
.admin-header__crumb {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.admin-header__crumb-kicker {
display: none;
font-size: 12px;
font-weight: 500;
color: #94a3b8;
}
@media (min-width: 640px) {
.admin-header__crumb-kicker {
display: inline;
}
}
.admin-header__crumb-sep {
display: none;
color: #cbd5e1;
}
@media (min-width: 640px) {
.admin-header__crumb-sep {
display: inline;
}
}
.admin-header__crumb-page {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 15px;
font-weight: 700;
color: #0f172a;
}
.admin-header__site {
display: inline-flex;
align-items: center;
gap: 8px;
height: 36px;
padding-inline: 8px;
border: none !important;
background: transparent !important;
color: #475569;
font-size: 13px;
font-weight: 600;
text-decoration: none;
transition: color 0.18s ease;
}
.admin-header__site:hover {
color: #ff5a00;
border: none !important;
background: transparent !important;
box-shadow: none !important;
}
.admin-header__divider {
width: 1px;
height: 28px;
background: #e8ecf2;
}
.admin-header__user {
display: inline-flex;
align-items: center;
gap: 10px;
max-width: 260px;
height: 44px;
padding-block: 4px;
padding-inline: 4px 10px;
border: 1px solid #e8ecf2;
border-radius: var(--ant-border-radius, 8px);
background: #f8fafc;
color: inherit;
cursor: pointer;
transition:
border-color 0.18s ease,
background 0.18s ease,
box-shadow 0.18s ease;
}
.admin-header__user:hover {
background: #ffffff;
border-color: #ffd0b5;
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.05);
}
.admin-header__avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
flex-shrink: 0;
border-radius: calc(var(--ant-border-radius, 8px) - 2px);
background: linear-gradient(135deg, #ff5a00, #ff8a3d);
color: #ffffff;
font-size: 14px;
font-weight: 700;
box-shadow: 0 2px 8px rgba(255, 90, 0, 0.28);
}
.admin-header__user-meta {
display: none;
flex-direction: column;
align-items: flex-start;
min-width: 0;
line-height: 1.25;
}
@media (min-width: 640px) {
.admin-header__user-meta {
display: flex;
}
}
.admin-header__user-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 140px;
font-size: 13px;
font-weight: 700;
color: #0f172a;
}
.admin-header__user-role {
font-size: 11px;
font-weight: 500;
color: #94a3b8;
}
.admin-header__chevron {
font-size: 10px;
color: #94a3b8;
}
.admin-sider {
height: 100vh !important;
height: 100dvh !important;
max-height: 100vh !important;
max-height: 100dvh !important;
position: sticky !important;
top: 0;
background: #ffffff !important;
/* content-facing edge (left in RTL) — continuous vertical rail */
border-inline-end: 1px solid #e8ecf2;
box-shadow: -4px 0 24px rgba(15, 23, 42, 0.04);
}
.admin-sider :deep(.ant-layout-sider-children) {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
overflow: hidden;
}
.admin-sider__brand {
display: flex;
flex-shrink: 0;
align-items: center;
gap: 10px;
height: 64px;
padding: 0 16px;
border-bottom: 1px solid #eef1f6;
box-sizing: border-box;
}
.admin-sider__logo {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 10px;
background: #fff7f2;
border: 1px solid #ffe0cc;
box-shadow: 0 1px 2px rgba(255, 90, 0, 0.06);
}
.admin-sider__nav {
flex: 1;
min-height: 0;
height: calc(100vh - 64px);
height: calc(100dvh - 64px);
padding: 16px 12px 20px;
overflow-x: hidden;
overflow-y: auto;
}
.admin-sider__label {
margin: 0 8px 10px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
color: #94a3b8;
}
.admin-menu {
background: transparent !important;
border: 0 !important;
}
.admin-menu :deep(.ant-menu-item) {
display: flex !important;
align-items: center !important;
height: 48px !important;
margin: 0 0 6px !important;
padding-inline: 10px 14px !important;
border: 1px solid transparent !important;
border-radius: 12px !important;
color: #475569 !important;
font-weight: 500;
line-height: 1.5 !important;
transition:
background 0.2s ease,
color 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
}
.admin-menu :deep(.ant-menu-item-icon) {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 36px !important;
height: 36px !important;
min-width: 36px !important;
min-height: 36px !important;
margin: 0 !important;
margin-inline-end: 12px !important;
padding: 0 !important;
font-size: 0 !important;
line-height: 0 !important;
background: transparent !important;
box-shadow: none !important;
flex-shrink: 0 !important;
}
.admin-menu :deep(.admin-menu__icon) {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 36px !important;
height: 36px !important;
min-width: 36px !important;
min-height: 36px !important;
margin: 0 !important;
padding: 0 !important;
flex-shrink: 0 !important;
border-radius: 10px !important;
background: #f1f5f9;
color: #64748b;
line-height: 0 !important;
transition:
background 0.2s ease,
color 0.2s ease,
box-shadow 0.2s ease;
}
.admin-menu :deep(.admin-menu__icon .vx-icon),
.admin-menu :deep(.admin-menu__icon .anticon) {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 20px !important;
height: 20px !important;
min-width: 20px !important;
min-height: 20px !important;
max-width: 20px !important;
max-height: 20px !important;
margin: 0 !important;
margin-inline-end: 0 !important;
margin-inline-start: 0 !important;
padding: 0 !important;
line-height: 0 !important;
vertical-align: 0 !important;
background: transparent !important;
box-shadow: none !important;
color: inherit !important;
}
.admin-menu :deep(.admin-menu__icon svg) {
width: 20px !important;
height: 20px !important;
max-width: 20px !important;
max-height: 20px !important;
display: block !important;
margin: 0 auto !important;
transform: none !important;
}
.admin-menu :deep(.ant-menu-title-content) {
display: flex !important;
align-items: center !important;
margin-inline-start: 0 !important;
line-height: 1.5 !important;
}
.admin-menu :deep(.ant-menu-item:hover) {
background: #f8fafc !important;
color: #0f172a !important;
border-color: #eef1f6 !important;
}
.admin-menu :deep(.ant-menu-item:hover:not(.ant-menu-item-selected) .admin-menu__icon) {
background: #e8eef7 !important;
color: #475569 !important;
}
.admin-menu :deep(.ant-menu-item-selected) {
background: #fff7f2 !important;
border-color: #ffd7bf !important;
color: #c2410c !important;
font-weight: 700;
box-shadow: 0 4px 14px rgba(255, 90, 0, 0.1);
}
.admin-menu :deep(.ant-menu-item-selected .admin-menu__icon) {
background: #ff5a00 !important;
color: #ffffff !important;
box-shadow: 0 4px 12px rgba(255, 90, 0, 0.35) !important;
}
.admin-menu :deep(.ant-menu-item-selected::after) {
display: none !important;
}
/* -------------------------------------------------------------
* Mobile App Experience Styles (Viewport < 1024px)
* ------------------------------------------------------------- */
.mobile-app-bar {
position: sticky;
top: 0;
z-index: 30;
display: flex;
align-items: center;
height: 52px;
padding-inline: 14px;
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(12px);
border-bottom: 1px solid #eef1f6;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.02);
}
.mobile-app-bar__back-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
margin-inline-start: -6px;
border: none;
background: transparent;
color: #334155;
border-radius: 8px;
cursor: pointer;
transition: background 0.15s ease;
}
.mobile-app-bar__back-btn:active {
background: #f1f5f9;
}
.mobile-nav-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 40;
display: flex;
align-items: center;
justify-content: space-around;
height: 60px;
padding-bottom: env(safe-area-inset-bottom, 0px);
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(16px);
border-top: 1px solid #e2e8f0;
box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.04);
}
.mobile-nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding-block: 4px;
border: none;
background: transparent;
color: #64748b;
cursor: pointer;
transition: color 0.15s ease;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.mobile-nav-item__icon {
display: flex;
align-items: center;
justify-content: center;
height: 24px;
transition: transform 0.15s ease;
}
.mobile-nav-item__label {
margin-top: 2px;
font-size: 10px;
font-weight: 600;
line-height: 1;
}
.mobile-nav-item--active {
color: #ff5a00;
}
.mobile-nav-item--active .mobile-nav-item__icon {
transform: translateY(-1px);
}
@media (max-width: 1023px) {
.admin-content {
padding: 0 !important;
background: #f8fafc !important;
overflow-y: auto !important;
-webkit-overflow-scrolling: touch;
}
.admin-content--has-bottom-nav {
padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px)) !important;
}
.admin-page {
border: none !important;
border-radius: 0 !important;
background: transparent !important;
box-shadow: none !important;
min-height: 100%;
}
.admin-page > :deep(.page-header) {
padding: 12px 14px !important;
border-bottom: 1px solid #eef1f6 !important;
background: #ffffff !important;
}
.admin-page > :deep(*:not(.ap):not(.page-header)) {
padding: 12px !important;
}
}
</style>