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

1051 lines
33 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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;
padding-inline-start: 8px !important;
padding-right: 8px !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>